diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml new file mode 100644 index 00000000..3bdfa0fa --- /dev/null +++ b/.github/workflows/unit.yml @@ -0,0 +1,30 @@ +name: Unit + +on: [push, pull_request] + +jobs: + ubuntu_makefile_escape: + name: Unix makefile escape + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup xmake + uses: xmake-io/github-action-setup-xmake@v1 + with: + xmake-version: '2.8.6' + + - name: Setup Qt + uses: ConorMacBride/install-package@v1 + with: + apt: libqt5svg5-dev qtbase5-dev qtbase5-dev-tools qttools5-dev-tools + + - name: Build + run: | + xmake f --qt=/usr + xmake b test-escape + + - name: Test + run: | + export QT_ASSUME_STDERR_HAS_CONSOLE=1 + xmake r test-escape diff --git a/NEWS.md b/NEWS.md index 309ab211..9b5875b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,9 @@ Red Panda C++ Version 2.27 - enhancement: Display ascii control chars. - fix: Parser: invalidating file may lost class inheritance infos. - fix: Function argument infos are not correctly parsed. + - enhancement: Migrate external calls from command string to argv array to improve safety and security. + - enhancement: Support POSIX shell-like escaping in user inputs for compiler arguments. + - fix: (Hopefully) properly escape filenames and arguments in makefile generation. Red Panda C++ Version 2.26 - enhancement: Code suggestion for embedded std::vectors. diff --git a/RedPandaIDE/RedPandaIDE.pro b/RedPandaIDE/RedPandaIDE.pro index 00984131..28c9ff5c 100644 --- a/RedPandaIDE/RedPandaIDE.pro +++ b/RedPandaIDE/RedPandaIDE.pro @@ -197,6 +197,8 @@ SOURCES += \ settingsdialog/settingswidget.cpp \ systemconsts.cpp \ utils.cpp \ + utils/escape.cpp \ + utils/parsearg.cpp \ widgets/coloredit.cpp \ widgets/compileargumentswidget.cpp \ widgets/consolewidget.cpp \ @@ -324,6 +326,8 @@ HEADERS += \ settingsdialog/settingswidget.h \ systemconsts.h \ utils.h \ + utils/escape.h \ + utils/parsearg.h \ common.h \ widgets/coloredit.h \ widgets/compileargumentswidget.h \ diff --git a/RedPandaIDE/compiler/compiler.cpp b/RedPandaIDE/compiler/compiler.cpp index 85596a06..6269a9b7 100644 --- a/RedPandaIDE/compiler/compiler.cpp +++ b/RedPandaIDE/compiler/compiler.cpp @@ -16,6 +16,8 @@ */ #include "compiler.h" #include "utils.h" +#include "utils/escape.h" +#include "utils/parsearg.h" #include "compilermanager.h" #include "../systemconsts.h" @@ -68,10 +70,11 @@ void Compiler::run() for(int i=0;i \"%3\"").arg(extractFileName(mExtraCompilersList[i]), mExtraArgumentsList[i], mExtraOutputFilesList[i])); + log(tr(" - Command: %1 > %2").arg(command, escapeArgumentForPlatformShell(mExtraOutputFilesList[i], false))); } runCommand(mExtraCompilersList[i],mExtraArgumentsList[i],mDirectory, pipedText(),mExtraOutputFilesList[i]); } @@ -331,9 +334,9 @@ void Compiler::stopCompile() mStop = true; } -QString Compiler::getCharsetArgument(const QByteArray& encoding,FileType fileType, bool checkSyntax) +QStringList Compiler::getCharsetArgument(const QByteArray& encoding,FileType fileType, bool checkSyntax) { - QString result; + QStringList result; bool forceExecUTF8=false; // test if force utf8 from autolink infos if ((fileType == FileType::CSource || @@ -383,21 +386,22 @@ QString Compiler::getCharsetArgument(const QByteArray& encoding,FileType fileTyp } //qDebug()< compileOptions; @@ -412,38 +416,36 @@ QString Compiler::getCCompileArguments(bool checkSyntax) PCompilerOption pOption = CompilerInfoManager::getCompilerOption(compilerSet()->compilerType(), key); if (pOption && pOption->isC && !pOption->isLinker) { if (pOption->type == CompilerOptionType::Checkbox) - result += " " + pOption->setting; + result << pOption->setting; else if (pOption->type == CompilerOptionType::Input) - result += " " + pOption->setting + " " + compileOptions[key]; + result += {pOption->setting, compileOptions[key]}; else { - result += " " + pOption->setting + compileOptions[key]; + result << pOption->setting + compileOptions[key]; } } } + QMap macros = devCppMacroVariables(); + if (compilerSet()->useCustomCompileParams() && !compilerSet()->customCompileParams().isEmpty()) { - QStringList params = textToLines(compilerSet()->customCompileParams()); - foreach(const QString& param, params) - result += " "+ parseMacros(param); + result << parseArguments(compilerSet()->customCompileParams(), macros, true); } if (mProject) { QString s = mProject->options().compilerCmd; if (!s.isEmpty()) { s.replace("_@@_", " "); - QStringList params = textToLines(s); - foreach(const QString& param, params) - result += " "+ parseMacros(param); + result << parseArguments(s, macros, true); } } return result; } -QString Compiler::getCppCompileArguments(bool checkSyntax) +QStringList Compiler::getCppCompileArguments(bool checkSyntax) { - QString result; + QStringList result; if (checkSyntax) { - result += " -fsyntax-only"; + result << "-fsyntax-only"; } QMap compileOptions; if (mProject && !mProject->options().compilerOptions.isEmpty()) { @@ -457,75 +459,73 @@ QString Compiler::getCppCompileArguments(bool checkSyntax) PCompilerOption pOption = CompilerInfoManager::getCompilerOption(compilerSet()->compilerType(), key); if (pOption && pOption->isCpp && !pOption->isLinker) { if (pOption->type == CompilerOptionType::Checkbox) - result += " " + pOption->setting; + result << pOption->setting; else if (pOption->type == CompilerOptionType::Input) - result += " " + pOption->setting + " " + compileOptions[key]; + result += {pOption->setting, compileOptions[key]}; else { - result += " " + pOption->setting + compileOptions[key]; + result << pOption->setting + compileOptions[key]; } } } + + QMap macros = devCppMacroVariables(); if (compilerSet()->useCustomCompileParams() && !compilerSet()->customCompileParams().isEmpty()) { - QStringList params = textToLines(compilerSet()->customCompileParams()); - foreach(const QString& param, params) - result += " "+ parseMacros(param); + result << parseArguments(compilerSet()->customCompileParams(), macros, true); } if (mProject) { QString s = mProject->options().cppCompilerCmd; if (!s.isEmpty()) { s.replace("_@@_", " "); - QStringList params = textToLines(s); - foreach(const QString& param, params) - result += " "+ parseMacros(param); + result << parseArguments(s, macros, true); } } return result; } -QString Compiler::getCIncludeArguments() +QStringList Compiler::getCIncludeArguments() { - QString result; + QStringList result; foreach (const QString& folder,compilerSet()->CIncludeDirs()) { - result += QString(" -I\"%1\"").arg(folder); + result << "-I" + folder; } return result; } -QString Compiler::getProjectIncludeArguments() +QStringList Compiler::getProjectIncludeArguments() { - QString result; + QStringList result; if (mProject) { foreach (const QString& folder,mProject->options().includeDirs) { - result += QString(" -I\"%1\"").arg(folder); + result << "-I" + folder; } // result += QString(" -I\"%1\"").arg(extractFilePath(mProject->filename())); } return result; } -QString Compiler::getCppIncludeArguments() +QStringList Compiler::getCppIncludeArguments() { - QString result; + QStringList result; foreach (const QString& folder,compilerSet()->CppIncludeDirs()) { - result += QString(" -I\"%1\"").arg(folder); + result << "-I" + folder; } return result; } -QString Compiler::getLibraryArguments(FileType fileType) +QStringList Compiler::getLibraryArguments(FileType fileType) { - QString result; + QStringList result; //Add libraries foreach (const QString& folder, compilerSet()->libDirs()) { - result += QString(" -L\"%1\"").arg(folder); + result << "-L" + folder; } //add libs added via project if (mProject) { foreach (const QString& folder, mProject->options().libDirs){ - result += QString(" -L\"%1\"").arg(folder); + result << "-L" + folder; } } @@ -547,9 +547,7 @@ QString Compiler::getLibraryArguments(FileType fileType) } if (waitCount<=10) { QSet parsedFiles; - result += parseFileIncludesForAutolink( - mFilename, - parsedFiles); + result += parseFileIncludesForAutolink(mFilename, parsedFiles); } } @@ -567,11 +565,11 @@ QString Compiler::getLibraryArguments(FileType fileType) PCompilerOption pOption = CompilerInfoManager::getCompilerOption(compilerSet()->compilerType(), key); if (pOption && pOption->isLinker) { if (pOption->type == CompilerOptionType::Checkbox) - result += " " + pOption->setting; + result << pOption->setting; else if (pOption->type == CompilerOptionType::Input) - result += " " + pOption->setting + " " + compileOptions[key]; + result += {pOption->setting, compileOptions[key]}; else { - result += " " + pOption->setting + compileOptions[key]; + result << pOption->setting + compileOptions[key]; } } } @@ -581,47 +579,43 @@ QString Compiler::getLibraryArguments(FileType fileType) QStringList params = textToLines(compilerSet()->customLinkParams()); if (!params.isEmpty()) { foreach(const QString& param, params) - result += " " + param; + result << param; } } if (mProject) { if (mProject->options().type == ProjectType::GUI) { - result += " -mwindows"; + result << "-mwindows"; } if (!mProject->options().linkerCmd.isEmpty()) { QString s = mProject->options().linkerCmd; if (!s.isEmpty()) { - s.replace("_@@_", " "); - QStringList params = textToLines(s); - if (!params.isEmpty()) { - foreach(const QString& param, params) - result += " " + param; - } + s.replace("_@@_", " "); // historical reason + result += parseArguments(s, {}, true); } } if (mProject->options().staticLink) - result += " -static"; + result << "-static"; } else { if (compilerSet()->staticLink()) { - result += " -static"; + result << "-static"; } } return result; } -QString Compiler::parseFileIncludesForAutolink( +QStringList Compiler::parseFileIncludesForAutolink( const QString &filename, QSet& parsedFiles) { - QString result; + QStringList result; if (parsedFiles.contains(filename)) return result; parsedFiles.insert(filename); PAutolink autolink = pAutolinkManager->getLink(filename); if (autolink) { - result += ' '+autolink->linkOption; + result += parseArgumentsWithoutVariables(autolink->linkOption); } QStringList includedFiles = mParserForFile->getFileDirectIncludes(filename); // log(QString("File %1 included:").arg(filename)); @@ -632,9 +626,7 @@ QString Compiler::parseFileIncludesForAutolink( for (int i=includedFiles.size()-1;i>=0;i--) { QString includeFilename = includedFiles[i]; - result += parseFileIncludesForAutolink( - includeFilename, - parsedFiles); + result += parseFileIncludesForAutolink(includeFilename, parsedFiles); } return result; } @@ -667,7 +659,7 @@ bool Compiler::parseForceUTF8ForAutolink(const QString &filename, QSet return false; } -void Compiler::runCommand(const QString &cmd, const QString &arguments, const QString &workingDir, const QByteArray& inputText, const QString& outputFile) +void Compiler::runCommand(const QString &cmd, const QStringList &arguments, const QString &workingDir, const QByteArray& inputText, const QString& outputFile) { QProcess process; mStop = false; @@ -692,7 +684,7 @@ void Compiler::runCommand(const QString &cmd, const QString &arguments, const Q env.insert("CFLAGS",""); env.insert("CXXFLAGS",""); process.setProcessEnvironment(env); - process.setArguments(splitProcessCommand(arguments)); + process.setArguments(arguments); process.setWorkingDirectory(workingDir); QFile output; if (!outputFile.isEmpty()) { @@ -773,6 +765,11 @@ void Compiler::runCommand(const QString &cmd, const QString &arguments, const Q output.close(); } +QString Compiler::escapeCommandForLog(const QString &cmd, const QStringList &arguments) +{ + return escapeCommandForPlatformShell(extractFileName(cmd), arguments); +} + PCppParser Compiler::parser() const { return mParserForFile; diff --git a/RedPandaIDE/compiler/compiler.h b/RedPandaIDE/compiler/compiler.h index 7949fb6e..852699ba 100644 --- a/RedPandaIDE/compiler/compiler.h +++ b/RedPandaIDE/compiler/compiler.h @@ -70,14 +70,14 @@ protected: virtual QByteArray pipedText(); virtual bool prepareForRebuild() = 0; virtual bool beforeRunExtraCommand(int idx); - virtual QString getCharsetArgument(const QByteArray& encoding, FileType fileType, bool onlyCheckSyntax); - virtual QString getCCompileArguments(bool checkSyntax); - virtual QString getCppCompileArguments(bool checkSyntax); - virtual QString getCIncludeArguments(); - virtual QString getProjectIncludeArguments(); - virtual QString getCppIncludeArguments(); - virtual QString getLibraryArguments(FileType fileType); - virtual QString parseFileIncludesForAutolink( + virtual QStringList getCharsetArgument(const QByteArray& encoding, FileType fileType, bool onlyCheckSyntax); + virtual QStringList getCCompileArguments(bool checkSyntax); + virtual QStringList getCppCompileArguments(bool checkSyntax); + virtual QStringList getCIncludeArguments(); + virtual QStringList getProjectIncludeArguments(); + virtual QStringList getCppIncludeArguments(); + virtual QStringList getLibraryArguments(FileType fileType); + virtual QStringList parseFileIncludesForAutolink( const QString& filename, QSet& parsedFiles); virtual bool parseForceUTF8ForAutolink( @@ -85,15 +85,16 @@ protected: QSet& parsedFiles); void log(const QString& msg); void error(const QString& msg); - void runCommand(const QString& cmd, const QString& arguments, const QString& workingDir, const QByteArray& inputText=QByteArray(), const QString& outputFile=QString()); + void runCommand(const QString& cmd, const QStringList& arguments, const QString& workingDir, const QByteArray& inputText=QByteArray(), const QString& outputFile=QString()); + QString escapeCommandForLog(const QString &cmd, const QStringList &arguments); protected: bool mOnlyCheckSyntax; QString mCompiler; - QString mArguments; - QStringList mExtraCompilersList; - QStringList mExtraArgumentsList; - QStringList mExtraOutputFilesList; + QStringList mArguments; + QList mExtraCompilersList; + QList mExtraArgumentsList; + QList mExtraOutputFilesList; QString mOutputFile; int mErrorCount; int mWarningCount; diff --git a/RedPandaIDE/compiler/compilermanager.cpp b/RedPandaIDE/compiler/compilermanager.cpp index f81931ef..7c3b9e4b 100644 --- a/RedPandaIDE/compiler/compilermanager.cpp +++ b/RedPandaIDE/compiler/compilermanager.cpp @@ -26,6 +26,7 @@ #include "executablerunner.h" #include "ojproblemcasesrunner.h" #include "utils.h" +#include "utils/parsearg.h" #include "../systemconsts.h" #include "../settings.h" #include @@ -267,7 +268,7 @@ void CompilerManager::run( QString::number(consoleFlag), sharedMemoryId, localizePath(filename) - } + splitProcessCommand(arguments); + } + parseArgumentsWithoutVariables(arguments); if (pSettings->environment().useCustomTerminal()) { auto [filename, args, fileOwner] = wrapCommandForTerminalEmulator( pSettings->environment().terminalPath(), @@ -285,7 +286,7 @@ void CompilerManager::run( } } else { //delete when thread finished - execRunner = new ExecutableRunner(filename,splitProcessCommand(arguments),workDir); + execRunner = new ExecutableRunner(filename, parseArgumentsWithoutVariables(arguments), workDir); } #else QStringList execArgs; @@ -310,19 +311,19 @@ void CompilerManager::run( sharedMemoryId, redirectInputFilename, localizePath(filename), - } + splitProcessCommand(arguments); + } + parseArgumentsWithoutVariables(arguments); } else { execArgs = QStringList{ consolePauserPath, QString::number(consoleFlag), sharedMemoryId, localizePath(filename), - } + splitProcessCommand(arguments); + } + parseArgumentsWithoutVariables(arguments); } } else { execArgs = QStringList{ localizePath(filename), - } + splitProcessCommand(arguments); + } + parseArgumentsWithoutVariables(arguments); } auto [filename, args, fileOwner] = wrapCommandForTerminalEmulator( pSettings->environment().terminalPath(), @@ -336,7 +337,7 @@ void CompilerManager::run( execRunner->setStartConsole(true); } else { //delete when thread finished - execRunner = new ExecutableRunner(filename,splitProcessCommand(arguments),workDir); + execRunner = new ExecutableRunner(filename, parseArgumentsWithoutVariables(arguments), workDir); } if (redirectInput) { execRunner->setRedirectInput(true); @@ -380,7 +381,7 @@ void CompilerManager::doRunProblem(const QString &filename, const QString &argum if (mRunner!=nullptr) { return; } - OJProblemCasesRunner * execRunner = new OJProblemCasesRunner(filename,splitProcessCommand(arguments),workDir,problemCases); + OJProblemCasesRunner * execRunner = new OJProblemCasesRunner(filename, parseArgumentsWithoutVariables(arguments), workDir, problemCases); mRunner = execRunner; if (pSettings->executor().enableCaseLimit()) { execRunner->setExecTimeout(pSettings->executor().caseTimeout()); diff --git a/RedPandaIDE/compiler/filecompiler.cpp b/RedPandaIDE/compiler/filecompiler.cpp index 070b650b..0847af81 100644 --- a/RedPandaIDE/compiler/filecompiler.cpp +++ b/RedPandaIDE/compiler/filecompiler.cpp @@ -66,20 +66,20 @@ bool FileCompiler::prepareForCompile() log(tr("- Compiler Set Name: %1").arg(compilerSet()->name())); log(""); FileType fileType = getFileType(mFilename); - mArguments = QString(" \"%1\"").arg(mFilename); + mArguments = QStringList{mFilename}; if (!mOnlyCheckSyntax) { switch(compilerSet()->compilationStage()) { case Settings::CompilerSet::CompilationStage::PreprocessingOnly: mOutputFile=changeFileExt(mFilename,compilerSet()->preprocessingSuffix()); - mArguments+=" -E"; + mArguments << "-E"; break; case Settings::CompilerSet::CompilationStage::CompilationProperOnly: mOutputFile=changeFileExt(mFilename,compilerSet()->compilationProperSuffix()); - mArguments+=" -S -fverbose-asm"; + mArguments += {"-S", "-fverbose-asm"}; break; case Settings::CompilerSet::CompilationStage::AssemblingOnly: mOutputFile=changeFileExt(mFilename,compilerSet()->assemblingSuffix()); - mArguments+=" -c"; + mArguments << "-c"; break; case Settings::CompilerSet::CompilationStage::GenerateExecutable: mOutputFile = changeFileExt(mFilename,compilerSet()->executableSuffix()); @@ -91,14 +91,14 @@ bool FileCompiler::prepareForCompile() } } #endif - mArguments+=QString(" -o \"%1\"").arg(mOutputFile); + mArguments += {"-o", mOutputFile}; #if defined(ARCH_X86_64) || defined(ARCH_X86) if (mCompileType == CppCompileType::GenerateAssemblyOnly) { if (pSettings->languages().noSEHDirectivesWhenGenerateASM()) - mArguments+=" -fno-asynchronous-unwind-tables"; + mArguments << "-fno-asynchronous-unwind-tables"; if (pSettings->languages().x86DialectOfASMGenerated()==Settings::Languages::X86ASMDialect::Intel) - mArguments+=" -masm=intel"; + mArguments << "-masm=intel"; } #endif //remove the old file if it exists @@ -171,7 +171,7 @@ bool FileCompiler::prepareForCompile() break; } if (hasStart) { - mArguments+=" -nostartfiles"; + mArguments << "-nostartfiles"; } } @@ -185,7 +185,8 @@ bool FileCompiler::prepareForCompile() log(tr("Processing %1 source file:").arg(strFileType)); log("------------------"); log(tr("%1 Compiler: %2").arg(strFileType).arg(mCompiler)); - log(tr("Command: %1 %2").arg(extractFileName(mCompiler)).arg(mArguments)); + QString command = escapeCommandForLog(mCompiler, mArguments); + log(tr("Command: %1").arg(command)); mDirectory = extractFileDir(mFilename); return true; } diff --git a/RedPandaIDE/compiler/projectcompiler.cpp b/RedPandaIDE/compiler/projectcompiler.cpp index a1e2ef36..fcc7769c 100644 --- a/RedPandaIDE/compiler/projectcompiler.cpp +++ b/RedPandaIDE/compiler/projectcompiler.cpp @@ -20,6 +20,10 @@ #include "../systemconsts.h" #include "qt_utils/charsetinfo.h" #include "../editor.h" +#include "qt_utils/utils.h" +#include "utils.h" +#include "utils/escape.h" +#include "utils/parsearg.h" #include @@ -51,12 +55,15 @@ void ProjectCompiler::createStandardMakeFile() { QFile file(mProject->makeFileName()); newMakeFile(file); - file.write("$(BIN): $(OBJ)\n"); + QString executable = extractRelativePath(mProject->makeFileName(), mProject->executable()); + QString exeTarget = escapeFilenameForMakefileTarget(executable); + QString exeCommand = escapeArgumentForMakefileRecipe(executable, false); + writeln(file, exeTarget + ": $(OBJ)\n"); if (!mOnlyCheckSyntax) { if (mProject->options().isCpp) { - writeln(file,"\t$(CPP) $(LINKOBJ) -o $(BIN) $(LIBS)"); + writeln(file, "\t$(CXX) $(LINKOBJ) -o " + exeCommand + " $(LIBS)"); } else - writeln(file,"\t$(CC) $(LINKOBJ) -o $(BIN) $(LIBS)"); + writeln(file, "\t$(CC) $(LINKOBJ) -o " + exeCommand + " $(LIBS)"); } writeMakeObjFilesRules(file); } @@ -65,9 +72,12 @@ void ProjectCompiler::createStaticMakeFile() { QFile file(mProject->makeFileName()); newMakeFile(file); - writeln(file,"$(BIN): $(LINKOBJ)"); - writeln(file,"\tar r $(BIN) $(LINKOBJ)"); - writeln(file,"\tranlib $(BIN)"); + QString executable = extractRelativePath(mProject->makeFileName(), mProject->executable()); + QString exeTarget = escapeFilenameForMakefileTarget(executable); + QString exeCommand = escapeArgumentForMakefileRecipe(executable, false); + writeln(file, exeTarget + ": $(LINKOBJ)"); + writeln(file, "\tar r " + exeCommand + " $(LINKOBJ)"); + writeln(file, "\tranlib " + exeCommand); writeMakeObjFilesRules(file); } @@ -75,11 +85,14 @@ void ProjectCompiler::createDynamicMakeFile() { QFile file(mProject->makeFileName()); newMakeFile(file); - writeln(file,"$(BIN): $(LINKOBJ)"); + QString executable = extractRelativePath(mProject->makeFileName(), mProject->executable()); + QString exeTarget = escapeFilenameForMakefileTarget(executable); + QString exeCommand = escapeArgumentForMakefileRecipe(executable, false); + writeln(file, exeTarget + ": $(LINKOBJ)"); if (mProject->options().isCpp) { - writeln(file, "\t$(CPP) -mdll $(LINKOBJ) -o $(BIN) $(LIBS) -Wl,--output-def,$(DEF),--out-implib,$(STATIC)"); + writeln(file, "\t$(CXX) -mdll $(LINKOBJ) -o " + exeCommand + " $(LIBS) -Wl,--output-def,$(DEF),--out-implib,$(STATIC)"); } else { - writeln(file, "\t$(CC) -mdll $(LINKOBJ) -o $(BIN) $(LIBS) -Wl,--output-def,$(DEF),--out-implib,$(STATIC)"); + writeln(file, "\t$(CC) -mdll $(LINKOBJ) -o " + exeCommand + " $(LIBS) -Wl,--output-def,$(DEF),--out-implib,$(STATIC)"); } writeMakeObjFilesRules(file); } @@ -121,8 +134,13 @@ void ProjectCompiler::newMakeFile(QFile& file) // PCH if (mProject->options().usePrecompiledHeader && fileExists(mProject->options().precompiledHeader)) { - writeln(file, "$(PCH) : $(PCH_H)"); - writeln(file, "\t$(CPP) -c $(PCH_H) -o $(PCH) $(CXXFLAGS)"); + QString pchH = extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader); + QString pchHCommand = escapeArgumentForMakefileRecipe(pchH, false); + QString pch = extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader + "." GCH_EXT); + QString pchTarget = escapeFilenameForMakefileTarget(pch); + QString pchCommand = escapeArgumentForMakefileRecipe(pch, false); + writeln(file, pchTarget + ": $(PCH_H)"); + writeln(file, "\t$(CXX) -c " + pchHCommand + " -o " + pchCommand + " $(CXXFLAGS)"); writeln(file); } } @@ -137,9 +155,9 @@ void ProjectCompiler::writeMakeHeader(QFile &file) void ProjectCompiler::writeMakeDefines(QFile &file) { // Get list of object files - QString Objects; - QString LinkObjects; - QString cleanObjects; + QStringList Objects; + QStringList LinkObjects; + QStringList cleanObjects; // Create a list of object files foreach(const PProjectUnit &unit, mProject->unitList()) { @@ -157,123 +175,114 @@ void ProjectCompiler::writeMakeDefines(QFile &file) QString fullObjFile = includeTrailingPathDelimiter(mProject->options().objectOutput) + extractFileName(unit->fileName()); QString relativeObjFile = extractRelativePath(mProject->directory(), changeFileExt(fullObjFile, OBJ_EXT)); - QString objFile = genMakePath2(relativeObjFile); - Objects += ' ' + objFile; -#ifdef Q_OS_WIN - cleanObjects += ' ' + genMakePath1(relativeObjFile).replace("/",QDir::separator()); -#else - cleanObjects += ' ' + genMakePath1(relativeObjFile); -#endif + Objects << relativeObjFile; + cleanObjects << localizePath(relativeObjFile); if (unit->link()) { - LinkObjects += ' ' + genMakePath1(relativeObjFile); + LinkObjects << relativeObjFile; } } else { - Objects += ' ' + genMakePath2(changeFileExt(RelativeName, OBJ_EXT)); -#ifdef Q_OS_WIN - cleanObjects += ' ' + genMakePath1(changeFileExt(RelativeName, OBJ_EXT)).replace("/",QDir::separator()); -#else - cleanObjects += ' ' + genMakePath1(changeFileExt(RelativeName, OBJ_EXT)); -#endif + Objects << changeFileExt(RelativeName, OBJ_EXT); + cleanObjects << localizePath(changeFileExt(RelativeName, OBJ_EXT)); if (unit->link()) - LinkObjects = LinkObjects + ' ' + genMakePath1(changeFileExt(RelativeName, OBJ_EXT)); + LinkObjects << changeFileExt(RelativeName, OBJ_EXT); } } } - Objects = Objects.trimmed(); - LinkObjects = LinkObjects.trimmed(); - // Get windres file QString objResFile; - QString objResFile2; QString cleanRes; #ifdef Q_OS_WIN if (!mProject->options().privateResource.isEmpty()) { - if (!mProject->options().objectOutput.isEmpty()) { - QString fullResFile = includeTrailingPathDelimiter(mProject->options().objectOutput) + - changeFileExt(mProject->options().privateResource, RES_EXT); + if (!mProject->options().objectOutput.isEmpty()) { + QString fullResFile = includeTrailingPathDelimiter(mProject->options().objectOutput) + + changeFileExt(mProject->options().privateResource, RES_EXT); - QString relativeResFile = extractRelativePath(mProject->directory(), fullResFile); - objResFile = genMakePath1(relativeResFile); - objResFile2 = genMakePath2(relativeResFile); - cleanRes += ' ' + genMakePath1(changeFileExt(relativeResFile, RES_EXT)).replace("/",QDir::separator()); - } else { - objResFile = genMakePath1(changeFileExt(mProject->options().privateResource, RES_EXT)); - objResFile2 = genMakePath2(changeFileExt(mProject->options().privateResource, RES_EXT)); - cleanRes += ' ' + genMakePath1(changeFileExt(mProject->options().privateResource, RES_EXT)).replace("/",QDir::separator()); - } -} + QString relativeResFile = extractRelativePath(mProject->directory(), fullResFile); + objResFile = relativeResFile; + cleanRes = localizePath(changeFileExt(relativeResFile, RES_EXT)); + } else { + objResFile = changeFileExt(mProject->options().privateResource, RES_EXT); + cleanRes = localizePath(changeFileExt(mProject->options().privateResource, RES_EXT)); + } + } #endif // Mention progress in the logs if (!objResFile.isEmpty()) { - log(tr("- Resource File: %1").arg(generateAbsolutePath(mProject->directory(),objResFile))); + QString absolutePath = generateAbsolutePath(mProject->directory(),objResFile); + QString escaped = escapeArgumentForPlatformShell(absolutePath, false); + log(tr("- Resource File: %1").arg(escaped)); } log(""); + QString cxx = extractFileName(compilerSet()->cppCompiler()); + QString cc = extractFileName(compilerSet()->CCompiler()); +#ifdef Q_OS_WIN + QString windres = extractFileName(compilerSet()->resourceCompiler()); +#endif + // Get list of applicable flags - QString cCompileArguments = getCCompileArguments(false); - QString cppCompileArguments = getCppCompileArguments(false); - QString libraryArguments = getLibraryArguments(FileType::Project); - QString cIncludeArguments = getCIncludeArguments() + " " + getProjectIncludeArguments(); - QString cppIncludeArguments = getCppIncludeArguments() + " " +getProjectIncludeArguments(); - - if (cCompileArguments.indexOf(" -g3")>=0 - || cCompileArguments.startsWith("-g3")) { - cCompileArguments += " -D__DEBUG__"; - cppCompileArguments+= " -D__DEBUG__"; + QStringList cCompileArguments = getCCompileArguments(false); + QStringList cxxCompileArguments = getCppCompileArguments(false); + if (cCompileArguments.contains("-g3")) { + cCompileArguments << "-D__DEBUG__"; + cxxCompileArguments << "-D__DEBUG__"; } + QStringList libraryArguments = getLibraryArguments(FileType::Project); + QStringList cIncludeArguments = getCIncludeArguments(); + QStringList cxxIncludeArguments = getCppIncludeArguments(); +#ifdef Q_OS_WIN + QStringList resourceArguments = parseArguments(mProject->options().resourceCmd, devCppMacroVariables(), true); +#endif - writeln(file,"CPP = " + extractFileName(compilerSet()->cppCompiler())); - writeln(file,"CC = " + extractFileName(compilerSet()->CCompiler())); + QString executable = extractRelativePath(mProject->makeFileName(), mProject->executable()); + QString cleanExe = localizePath(executable); + QString pchH = extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader); + QString pch = extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader + "." GCH_EXT); + + // programs + writeln(file, "CXX = " + escapeArgumentForMakefileVariableValue(cxx, true)); + writeln(file, "CC = " + escapeArgumentForMakefileVariableValue(cc, true)); #ifdef Q_OS_WIN - writeln(file,"WINDRES = " + extractFileName(compilerSet()->resourceCompiler())); + writeln(file, "WINDRES = " + escapeArgumentForMakefileVariableValue(windres, true)); #endif + writeln(file, "RM = " CLEAN_PROGRAM); + + // compiler flags + writeln(file, "LIBS = " + escapeArgumentsForMakefileVariableValue(libraryArguments)); + writeln(file, "INCS = " + escapeArgumentsForMakefileVariableValue(cIncludeArguments)); + writeln(file, "CXXINCS = " + escapeArgumentsForMakefileVariableValue(cxxIncludeArguments)); + writeln(file, "CXXFLAGS = $(CXXINCS) " + escapeArgumentsForMakefileVariableValue(cxxCompileArguments)); + writeln(file, "CFLAGS = $(INCS) " + escapeArgumentsForMakefileVariableValue(cCompileArguments)); +#ifdef Q_OS_WIN + writeln(file, "WINDRESFLAGS = " + escapeArgumentsForMakefileVariableValue(resourceArguments)); +#endif + + // objects referenced in prerequisites + // do not use them in targets or command arguments, they have different escaping rules if (!objResFile.isEmpty()) { - writeln(file,"RES = " + objResFile2); - writeln(file,"OBJ = " + Objects + " $(RES)"); - writeln(file,"LINKOBJ = " + LinkObjects + " " + objResFile); -#ifdef Q_OS_WIN - writeln(file,"CLEANOBJ = " + cleanObjects + - " " + cleanRes - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable())).replace("/",QDir::separator()) ); -#else - writeln(file,"CLEANOBJ = " + cleanObjects + - " " + cleanRes - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable()))); -#endif + writeln(file, "RES = " + escapeFilenameForMakefilePrerequisite(objResFile)); + writeln(file, "OBJ = " + escapeFilenamesForMakefilePrerequisite(Objects) + " $(RES)"); } else { - writeln(file,"OBJ = " + Objects); - writeln(file,"LINKOBJ = " + LinkObjects); -#ifdef Q_OS_WIN - writeln(file,"CLEANOBJ = " + cleanObjects + - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable())).replace("/",QDir::separator()) ); -#else - writeln(file,"CLEANOBJ = " + cleanObjects + - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable()))); -#endif + writeln(file, "OBJ = " + escapeFilenamesForMakefilePrerequisite(Objects)); }; - libraryArguments.replace('\\', '/'); - writeln(file,"LIBS = " + libraryArguments); - cIncludeArguments.replace('\\', '/'); - writeln(file,"INCS = " + cIncludeArguments); - cppIncludeArguments.replace('\\', '/'); - writeln(file,"CXXINCS = " + cppIncludeArguments); - writeln(file,"BIN = " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable()))); - cppCompileArguments.replace('\\', '/'); - writeln(file,"CXXFLAGS = $(CXXINCS) " + cppCompileArguments); - //writeln(file,"ENCODINGS = -finput-charset=utf-8 -fexec-charset='+GetSystemCharsetName); - cCompileArguments.replace('\\', '/'); - writeln(file,"CFLAGS = $(INCS) " + cCompileArguments); - writeln(file, QString("RM = ") + CLEAN_PROGRAM ); + writeln(file, "BIN = " + escapeFilenameForMakefilePrerequisite(executable)); if (mProject->options().usePrecompiledHeader && fileExists(mProject->options().precompiledHeader)){ - writeln(file,"PCH_H = " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader ))); - writeln(file,"PCH = " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader+"."+GCH_EXT))); - } -#ifdef Q_OS_WIN - writeln(file,"WINDRESFLAGS = " + mProject->options().resourceCmd); -#endif + writeln(file, "PCH_H = " + escapeFilenameForMakefilePrerequisite(pchH)); + writeln(file, "PCH = " + escapeFilenameForMakefilePrerequisite(pch)); + } + + // object referenced in command arguments + // use them in targets or prerequisites, they have different escaping rules + if (!objResFile.isEmpty()) { + writeln(file, "LINKOBJ = " + escapeArgumentsForMakefileVariableValue(LinkObjects) + " " + escapeArgumentForMakefileVariableValue(objResFile, false)); + writeln(file, "CLEANOBJ = " + escapeArgumentsForMakefileVariableValue(cleanObjects) + " " + escapeArgumentForMakefileVariableValue(cleanRes, false) + " " + escapeArgumentForMakefileVariableValue(cleanExe, false)); + } else { + writeln(file, "LINKOBJ = " + escapeArgumentsForMakefileVariableValue(LinkObjects)); + writeln(file, "CLEANOBJ = " + escapeArgumentsForMakefileVariableValue(cleanObjects) + " " + escapeArgumentForMakefileVariableValue(cleanExe, false)); + }; // This needs to be put in before the clean command. if (mProject->options().type == ProjectType::DynamicLib) { @@ -290,15 +299,12 @@ void ProjectCompiler::writeMakeDefines(QFile &file) libOutputFile = extractFileName(libOutputFile); else libOutputFile = extractRelativePath(mProject->makeFileName(), libOutputFile); - writeln(file,"DEF = " + genMakePath1(changeFileExt(libOutputFile, DEF_EXT))); - writeln(file,"STATIC = " + genMakePath1(changeFileExt(libOutputFile, LIB_EXT))); -#ifdef Q_OS_WIN - writeln(file,"CLEAN_DEF = " + genMakePath1(changeFileExt(libOutputFile, DEF_EXT)).replace("/",QDir::separator())); - writeln(file,"CLEAN_STATIC = " + genMakePath1(changeFileExt(libOutputFile, LIB_EXT)).replace("/",QDir::separator())); -#else - writeln(file,"CLEAN_DEF = " + genMakePath1(changeFileExt(libOutputFile, DEF_EXT))); - writeln(file,"CLEAN_STATIC = " + genMakePath1(changeFileExt(libOutputFile, LIB_EXT))); -#endif + + QString defFile = localizePath(changeFileExt(libOutputFile, DEF_EXT)); + QString staticFile = localizePath(changeFileExt(libOutputFile, LIB_EXT)); + + writeln(file,"DEF = " + escapeArgumentForMakefileVariableValue(defFile, false)); + writeln(file,"STATIC = " + escapeArgumentForMakefileVariableValue(staticFile, false)); } writeln(file); } @@ -315,7 +321,7 @@ void ProjectCompiler::writeMakeTarget(QFile &file) void ProjectCompiler::writeMakeIncludes(QFile &file) { foreach(const QString& s, mProject->options().makeIncludes) { - writeln(file, "include " + genMakePath1(s)); + writeln(file, "include " + escapeFilenameForMakefileInclude(s)); } if (!mProject->options().makeIncludes.isEmpty()) { writeln(file); @@ -328,11 +334,12 @@ void ProjectCompiler::writeMakeClean(QFile &file) QString target="$(CLEANOBJ)"; if (mProject->options().usePrecompiledHeader && fileExists(mProject->options().precompiledHeader)) { - target += " $(PCH)"; + QString pch = extractRelativePath(mProject->makeFileName(), mProject->options().precompiledHeader + "." GCH_EXT); + target += ' ' + escapeArgumentForMakefileRecipe(pch, false); } if (mProject->options().type == ProjectType::DynamicLib) { - target +=" $(CLEAN_DEF) $(CLEAN_STATIC)"; + target +=" $(DEF) $(STATIC)"; } writeln(file, QString("\t-$(RM) %1 > %2 2>&1").arg(target,NULL_FILE)); writeln(file); @@ -356,7 +363,7 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) QString shortFileName = extractRelativePath(mProject->makeFileName(),unit->fileName()); writeln(file); - QString objStr=genMakePath2(shortFileName); + QString objStr = escapeFilenameForMakefilePrerequisite(shortFileName); // if we have scanned it, use scanned info if (parser && parser->fileScanned(unit->fileName())) { QSet fileIncludes = parser->getIncludedFiles(unit->fileName()); @@ -367,33 +374,36 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) if (mProject->options().usePrecompiledHeader && unit2->fileName() == mProject->options().precompiledHeader) precompileStr = " $(PCH) "; - else - objStr = objStr + ' ' + genMakePath2(extractRelativePath(mProject->makeFileName(),unit2->fileName())); + else { + QString prereq = extractRelativePath(mProject->makeFileName(), unit2->fileName()); + objStr = objStr + ' ' + escapeFilenameForMakefilePrerequisite(prereq); + } } } } else { foreach(const PProjectUnit &unit2, projectUnits) { FileType fileType = getFileType(unit2->fileName()); - if (fileType == FileType::CHeader || fileType==FileType::CppHeader) - objStr = objStr + ' ' + genMakePath2(extractRelativePath(mProject->makeFileName(),unit2->fileName())); + if (fileType == FileType::CHeader || fileType==FileType::CppHeader) { + QString prereq = extractRelativePath(mProject->makeFileName(), unit2->fileName()); + objStr = objStr + ' ' + escapeFilenameForMakefilePrerequisite(prereq); + } } } - QString objFileName; - QString objFileName2; + QString objFileNameTarget; + QString objFileNameCommand; if (!mProject->options().objectOutput.isEmpty()) { QString fullObjname = includeTrailingPathDelimiter(mProject->options().objectOutput) + extractFileName(unit->fileName()); - objFileName = genMakePath2(extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, OBJ_EXT))); - objFileName2 = genMakePath1(extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, OBJ_EXT))); -// if (!extractFileDir(ObjFileName).isEmpty()) { -// objStr = genMakePath2(includeTrailingPathDelimiter(extractFileDir(ObjFileName))) + objStr; -// } + QString objectFile = extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, OBJ_EXT)); + objFileNameTarget = escapeFilenameForMakefileTarget(objectFile); + objFileNameCommand = escapeArgumentForMakefileRecipe(objectFile, false); } else { - objFileName = genMakePath2(changeFileExt(shortFileName, OBJ_EXT)); - objFileName2 = genMakePath1(changeFileExt(shortFileName, OBJ_EXT)); + QString objectFile = changeFileExt(shortFileName, OBJ_EXT); + objFileNameTarget = escapeFilenameForMakefileTarget(objectFile); + objFileNameCommand = escapeArgumentForMakefileRecipe(objectFile, false); } - objStr = objFileName + ": "+objStr+precompileStr; + objStr = objFileNameTarget + ": " + objStr + precompileStr; writeln(file,objStr); @@ -457,11 +467,11 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) if (fileType==FileType::CSource || fileType==FileType::CppSource) { if (unit->compileCpp()) - writeln(file, "\t$(CPP) -c " + genMakePath1(shortFileName) + " -o " + objFileName2 + " $(CXXFLAGS) " + encodingStr); + writeln(file, "\t$(CXX) -c " + escapeArgumentForMakefileRecipe(shortFileName, false) + " -o " + objFileNameCommand + " $(CXXFLAGS) " + encodingStr); else - writeln(file, "\t$(CC) -c " + genMakePath1(shortFileName) + " -o " + objFileName2 + " $(CFLAGS) " + encodingStr); + writeln(file, "\t$(CC) -c " + escapeArgumentForMakefileRecipe(shortFileName, false) + " -o " + objFileNameCommand + " $(CFLAGS) " + encodingStr); } else if (fileType==FileType::GAS) { - writeln(file, "\t$(CC) -c " + genMakePath1(shortFileName) + " -o " + objFileName2 + " $(CFLAGS) " + encodingStr); + writeln(file, "\t$(CC) -c " + escapeArgumentForMakefileRecipe(shortFileName, false) + " -o " + objFileNameCommand + " $(CFLAGS) " + encodingStr); } } } @@ -473,7 +483,7 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) for (int i=0;ioptions().resourceIncludes.count();i++) { QString filename = mProject->options().resourceIncludes[i]; if (!filename.isEmpty()) - ResIncludes = ResIncludes + " --include-dir " + genMakePath1(filename); + ResIncludes += " --include-dir " + escapeArgumentForMakefileRecipe(filename, false); } QString resFiles; @@ -483,10 +493,9 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) continue; if (fileExists(unit->fileName())) { QString ResFile = extractRelativePath(mProject->makeFileName(), unit->fileName()); - resFiles = resFiles + genMakePath2(ResFile) + ' '; + resFiles = resFiles + escapeFilenameForMakefilePrerequisite(ResFile) + ' '; } } - resFiles = resFiles.trimmed(); // Determine resource output file QString fullName; @@ -496,10 +505,12 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) } else { fullName = changeFileExt(mProject->options().privateResource, RES_EXT); } - QString objFileName = genMakePath1(extractRelativePath(mProject->filename(), fullName)); - QString objFileName2 = genMakePath2(extractRelativePath(mProject->filename(), fullName)); - QString privResName = genMakePath1(extractRelativePath(mProject->filename(), mProject->options().privateResource)); - QString privResName2 = genMakePath2(extractRelativePath(mProject->filename(), mProject->options().privateResource)); + QString objFile = extractRelativePath(mProject->filename(), fullName); + QString objFileNameCommand = escapeArgumentForMakefileRecipe(objFile, false); + QString objFileNameTarget = escapeFilenameForMakefileTarget(objFile); + QString privRes = extractRelativePath(mProject->filename(), mProject->options().privateResource); + QString privResNameCommand = escapeArgumentForMakefileRecipe(privRes, false); + QString privResNamePrereq = escapeFilenameForMakefilePrerequisite(privRes); // Build final cmd QString windresArgs; @@ -508,8 +519,8 @@ void ProjectCompiler::writeMakeObjFilesRules(QFile &file) windresArgs = " -F pe-i386"; writeln(file); - writeln(file, objFileName2 + ": " + privResName2 + ' ' + resFiles); - writeln(file, "\t$(WINDRES) -i " + privResName + windresArgs + " --input-format=rc -o " + objFileName + " -O coff $(WINDRESFLAGS)" + writeln(file, objFileNameTarget + ": " + privResNamePrereq + ' ' + resFiles); + writeln(file, "\t$(WINDRES) -i " + privResNameCommand + windresArgs + " --input-format=rc -o " + objFileNameCommand + " -O coff $(WINDRESFLAGS)" + ResIncludes); writeln(file); } @@ -564,39 +575,43 @@ bool ProjectCompiler::prepareForCompile() QString parallelParam; if (mProject->options().allowParallelBuilding) { if (mProject->options().parellelBuildingJobs==0) { - parallelParam = " --jobs"; + parallelParam = "--jobs"; } else { - parallelParam = QString(" -j%1").arg(mProject->options().parellelBuildingJobs); + parallelParam = QString("-j%1").arg(mProject->options().parellelBuildingJobs); } + } else { + parallelParam = "-j1"; } + QString makefile = extractRelativePath(mProject->directory(), mProject->makeFileName()); + QStringList cleanArgs{ + "-f", + makefile, + "clean", + }; + QStringList makeAllArgs{ + parallelParam, + "-f", + makefile, + "all", + }; if (mOnlyClean) { - mArguments = QString(" %1 -f \"%2\" clean").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName())); + mArguments = cleanArgs; } else if (mRebuild) { - mArguments = QString(" -f \"%1\" clean").arg(extractRelativePath( - mProject->directory(), - mProject->makeFileName())); - mExtraCompilersList.append(mCompiler); - mExtraOutputFilesList.append(""); - mExtraArgumentsList.append(QString(" %1 -f \"%2\" all").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName()))); + mArguments = cleanArgs; + mExtraCompilersList << mCompiler; + mExtraOutputFilesList << ""; + mExtraArgumentsList << makeAllArgs; } else { - mArguments = QString(" %1 -f \"%2\" all").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName())); + mArguments = makeAllArgs; } mDirectory = mProject->directory(); log(tr("Processing makefile:")); log("--------"); log(tr("- makefile processer: %1").arg(mCompiler)); - log(tr("- Command: %1 %2").arg(extractFileName(mCompiler)).arg(mArguments)); + QString command = escapeCommandForLog(mCompiler, mArguments); + log(tr("- Command: %1").arg(command)); log(""); return true; diff --git a/RedPandaIDE/compiler/sdccfilecompiler.cpp b/RedPandaIDE/compiler/sdccfilecompiler.cpp index 1873eafa..01446634 100644 --- a/RedPandaIDE/compiler/sdccfilecompiler.cpp +++ b/RedPandaIDE/compiler/sdccfilecompiler.cpp @@ -43,7 +43,7 @@ bool SDCCFileCompiler::prepareForCompile() mArguments += getCCompileArguments(false); mArguments += getCIncludeArguments(); mArguments += getProjectIncludeArguments(); - mArguments = QString("--syntax-only \"%1\"").arg(mFilename); + mArguments += {"--syntax-only", mFilename}; mDirectory = extractFileDir(mFilename); return true; } @@ -73,16 +73,15 @@ bool SDCCFileCompiler::prepareForCompile() mNoStartup = (val==COMPILER_OPTION_ON); if (mNoStartup) { mRelFilename = changeFileExt(mFilename,SDCC_REL_SUFFIX); - mArguments += QString(" -c \"%1\"").arg(mFilename); - mExtraCompilersList.append(mCompiler); - QString args = getLibraryArguments(FileType::CSource); - args += QString(" -o \"%1\" \"%2\" ").arg(mIhxFilename, mRelFilename); - mExtraArgumentsList.append(args); - mExtraOutputFilesList.append(""); + mArguments += {"-c", mFilename}; + mExtraCompilersList << mCompiler; + QStringList args = getLibraryArguments(FileType::CSource); + args += {"-o", mIhxFilename, mRelFilename}; + mExtraArgumentsList << args; + mExtraOutputFilesList << ""; } else { mArguments += getLibraryArguments(FileType::CSource); - mArguments += QString(" \"%1\"").arg(mFilename); - mArguments+=QString(" -o \"%1\"").arg(mIhxFilename); + mArguments += {mFilename, "-o", mIhxFilename}; } if (compilerSet()->executableSuffix() == SDCC_HEX_SUFFIX) { @@ -92,28 +91,26 @@ bool SDCCFileCompiler::prepareForCompile() return false; } mExtraCompilersList.append(packihx); - QString args; - args = QString(" \"%1\"").arg(mIhxFilename); - mExtraArgumentsList.append(args); - mExtraOutputFilesList.append(mOutputFile); + QStringList args{mIhxFilename}; + mExtraArgumentsList << args; + mExtraOutputFilesList << mOutputFile; } else if (compilerSet()->executableSuffix() == SDCC_BIN_SUFFIX) { QString makebin = compilerSet()->findProgramInBinDirs(MAKEBIN_PROGRAM); if (makebin.isEmpty()) { error(tr("Can't find \"%1\".\n").arg(PACKIHX_PROGRAM)); return false; } - mExtraCompilersList.push_back(makebin); - QString args; - args = QString(" \"%1\"").arg(mIhxFilename); - args+=QString(" \"%1\"").arg(mOutputFile); - mExtraArgumentsList.push_back(args); - mExtraOutputFilesList.append(""); + mExtraCompilersList << makebin; + QStringList args{mIhxFilename, mOutputFile}; + mExtraArgumentsList << args; + mExtraOutputFilesList << ""; } log(tr("Processing %1 source file:").arg(strFileType)); log("------------------"); log(tr("- %1 Compiler: %2").arg(strFileType).arg(mCompiler)); - log(tr("- Command: %1 %2").arg(extractFileName(mCompiler)).arg(mArguments)); + QString command = escapeCommandForLog(mCompiler, mArguments); + log(tr("- Command: %1").arg(command)); mDirectory = extractFileDir(mFilename); mStartCompileTime = QDateTime::currentDateTime(); return true; diff --git a/RedPandaIDE/compiler/sdccprojectcompiler.cpp b/RedPandaIDE/compiler/sdccprojectcompiler.cpp index d471dadb..52cc6b53 100644 --- a/RedPandaIDE/compiler/sdccprojectcompiler.cpp +++ b/RedPandaIDE/compiler/sdccprojectcompiler.cpp @@ -18,6 +18,9 @@ #include "../project.h" #include "compilermanager.h" #include "../systemconsts.h" +#include "qt_utils/utils.h" +#include "utils.h" +#include "utils/escape.h" #include @@ -40,17 +43,17 @@ void SDCCProjectCompiler::createStandardMakeFile() newMakeFile(file); QString suffix = compilerSet()->executableSuffix(); if (suffix == SDCC_IHX_SUFFIX) { - writeln(file,"$(BIN): $(OBJ)"); - writeln(file,"\t$(CC) $(LIBS) -o $(BIN) $(LINKOBJ)"); + writeln(file,"$(BIN_TAR): $(OBJ)"); + writeln(file,"\t$(CC) $(LIBS) -o $(BIN_ARG) $(LINKOBJ)"); } else { - writeln(file,"$(IHX): $(OBJ)\n"); - writeln(file,"\t$(CC) $(LIBS) -o $(IHX) $(LINKOBJ)"); + writeln(file,"$(IHX_TAR): $(OBJ)\n"); + writeln(file,"\t$(CC) $(LIBS) -o $(IHX_ARG) $(LINKOBJ)"); if (suffix == SDCC_HEX_SUFFIX) { - writeln(file,"$(BIN): $(IHX)"); - writeln(file,"\t$(PACKIHX) $(IHX) > $(BIN)"); + writeln(file,"$(BIN_TAR): $(IHX_DEP)"); + writeln(file,"\t$(PACKIHX) $(IHX_ARG) > $(BIN_ARG)"); } else { - writeln(file,"$(BIN): $(IHX)"); - writeln(file,"\t$(MAKEBIN) $(IHX) $(BIN)"); + writeln(file,"$(BIN_TAR): $(IHX_DEP)"); + writeln(file,"\t$(MAKEBIN) $(IHX_ARG) $(BIN_ARG)"); } } writeMakeObjFilesRules(file); @@ -102,9 +105,9 @@ void SDCCProjectCompiler::writeMakeHeader(QFile &file) void SDCCProjectCompiler::writeMakeDefines(QFile &file) { // Get list of object files - QString Objects; - QString LinkObjects; - QString cleanObjects; + QStringList Objects; + QStringList LinkObjects; + QStringList cleanObjects; // Create a list of object files foreach(const PProjectUnit &unit, mProject->unitList()) { @@ -122,67 +125,52 @@ void SDCCProjectCompiler::writeMakeDefines(QFile &file) QString fullObjFile = includeTrailingPathDelimiter(mProject->options().objectOutput) + extractFileName(unit->fileName()); QString relativeObjFile = extractRelativePath(mProject->directory(), changeFileExt(fullObjFile, SDCC_REL_SUFFIX)); - QString objFile = genMakePath2(relativeObjFile); - Objects += ' ' + objFile; -#ifdef Q_OS_WIN - cleanObjects += ' ' + genMakePath1(relativeObjFile).replace("/",QDir::separator()); -#else - cleanObjects += ' ' + genMakePath1(relativeObjFile); -#endif + Objects << relativeObjFile; + cleanObjects << localizePath(relativeObjFile); if (unit->link()) { - LinkObjects += ' ' + genMakePath1(relativeObjFile); + LinkObjects << relativeObjFile; } } else { - Objects += ' ' + genMakePath2(changeFileExt(RelativeName, SDCC_REL_SUFFIX)); -#ifdef Q_OS_WIN - cleanObjects += ' ' + genMakePath1(changeFileExt(RelativeName, SDCC_REL_SUFFIX)).replace("/",QDir::separator()); -#else - cleanObjects += ' ' + genMakePath1(changeFileExt(RelativeName, SDCC_REL_SUFFIX)); -#endif + Objects << changeFileExt(RelativeName, SDCC_REL_SUFFIX); + cleanObjects << localizePath(changeFileExt(RelativeName, SDCC_REL_SUFFIX)); if (unit->link()) - LinkObjects = LinkObjects + ' ' + genMakePath1(changeFileExt(RelativeName, SDCC_REL_SUFFIX)); + LinkObjects << changeFileExt(RelativeName, SDCC_REL_SUFFIX); } } } - Objects = Objects.trimmed(); - LinkObjects = LinkObjects.trimmed(); + QString cc = extractFileName(compilerSet()->CCompiler()); + QStringList cCompileArguments = getCCompileArguments(mOnlyCheckSyntax); + if (cCompileArguments.contains("-g3")) + cCompileArguments << "-D__DEBUG__"; + QStringList libraryArguments = getLibraryArguments(FileType::Project); + QStringList cIncludeArguments = getCIncludeArguments() + getProjectIncludeArguments(); - // Get list of applicable flags - QString cCompileArguments = getCCompileArguments(mOnlyCheckSyntax); - QString libraryArguments = getLibraryArguments(FileType::Project); - QString cIncludeArguments = getCIncludeArguments() + " " + getProjectIncludeArguments(); + QString executable = extractRelativePath(mProject->makeFileName(), mProject->executable()); + QString cleanExe = localizePath(executable); + QString ihx = extractRelativePath(mProject->makeFileName(), changeFileExt(mProject->executable(), SDCC_IHX_SUFFIX)); + QString cleanIhx = localizePath(ihx); - if (cCompileArguments.indexOf(" -g3")>=0 - || cCompileArguments.startsWith("-g3")) { - cCompileArguments += " -D__DEBUG__"; - } + writeln(file, "CC = " + escapeArgumentForMakefileVariableValue(cc, true)); + writeln(file, "PACKIHX = " PACKIHX_PROGRAM); + writeln(file, "MAKEBIN = " MAKEBIN_PROGRAM); - writeln(file,"CC = " + extractFileName(compilerSet()->CCompiler())); - writeln(file,QString("PACKIHX = ") + PACKIHX_PROGRAM); - writeln(file,QString("MAKEBIN = ") + MAKEBIN_PROGRAM); - - writeln(file,"OBJ = " + Objects); - writeln(file,"LINKOBJ = " + LinkObjects); -#ifdef Q_OS_WIN - writeln(file,"CLEANOBJ = " + cleanObjects + - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), changeFileExt(mProject->executable(),SDCC_IHX_SUFFIX))).replace("/",QDir::separator()) - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable())).replace("/",QDir::separator()) ); -#else - writeln(file,"CLEANOBJ = " + cleanObjects + - + " " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable()))); -#endif - libraryArguments.replace('\\', '/'); - writeln(file,"LIBS = " + libraryArguments); - cIncludeArguments.replace('\\', '/'); - writeln(file,"INCS = " + cIncludeArguments); - writeln(file,"IHX = " + genMakePath1(extractRelativePath(mProject->makeFileName(), changeFileExt(mProject->executable(), SDCC_IHX_SUFFIX)))); - writeln(file,"BIN = " + genMakePath1(extractRelativePath(mProject->makeFileName(), mProject->executable()))); - //writeln(file,"ENCODINGS = -finput-charset=utf-8 -fexec-charset='+GetSystemCharsetName); - cCompileArguments.replace('\\', '/'); - writeln(file,"CFLAGS = $(INCS) " + cCompileArguments); - writeln(file, QString("RM = ") + CLEAN_PROGRAM ); + writeln(file, "OBJ = " + escapeFilenamesForMakefilePrerequisite(Objects)); + writeln(file, "LINKOBJ = " + escapeArgumentsForMakefileVariableValue(LinkObjects)); + writeln(file,"CLEANOBJ = " + escapeArgumentsForMakefileVariableValue(cleanObjects) + ' ' + + escapeArgumentForMakefileVariableValue(cleanIhx, false) + ' ' + + escapeArgumentForMakefileVariableValue(cleanExe, false)); + writeln(file, "LIBS = " + escapeArgumentsForMakefileVariableValue(libraryArguments)); + writeln(file, "INCS = " + escapeArgumentsForMakefileVariableValue(cIncludeArguments)); + writeln(file, "IHX_TAR = " + escapeFilenameForMakefileTarget(ihx)); + writeln(file, "IHX_DEP = " + escapeFilenameForMakefilePrerequisite(ihx)); + writeln(file, "IHX_ARG = " + escapeArgumentForMakefileVariableValue(ihx, false)); + writeln(file, "BIN_TAR = " + escapeFilenameForMakefileTarget(executable)); + writeln(file, "BIN_DEP = " + escapeFilenameForMakefilePrerequisite(executable)); + writeln(file, "BIN_ARG = " + escapeArgumentForMakefileVariableValue(executable, false)); + writeln(file, "CFLAGS = $(INCS) " + escapeArgumentsForMakefileVariableValue(cCompileArguments)); + writeln(file, "RM = " CLEAN_PROGRAM); writeln(file); } @@ -191,7 +179,7 @@ void SDCCProjectCompiler::writeMakeTarget(QFile &file) { writeln(file, ".PHONY: all all-before all-after clean clean-custom"); writeln(file); - writeln(file, "all: all-before $(BIN) all-after"); + writeln(file, "all: all-before $(BIN_DEP) all-after"); writeln(file); } @@ -199,7 +187,7 @@ void SDCCProjectCompiler::writeMakeTarget(QFile &file) void SDCCProjectCompiler::writeMakeIncludes(QFile &file) { foreach(const QString& s, mProject->options().makeIncludes) { - writeln(file, "include " + genMakePath1(s)); + writeln(file, "include " + escapeFilenameForMakefileInclude(s)); } if (!mProject->options().makeIncludes.isEmpty()) { writeln(file); @@ -209,16 +197,13 @@ void SDCCProjectCompiler::writeMakeIncludes(QFile &file) void SDCCProjectCompiler::writeMakeClean(QFile &file) { writeln(file, "clean: clean-custom"); - QString target="$(CLEANOBJ)"; - - writeln(file, QString("\t-$(RM) %1 > %2 2>&1").arg(target,NULL_FILE)); + writeln(file, QString("\t-$(RM) $(CLEANOBJ) > %1 2>&1").arg(NULL_FILE)); writeln(file); } void SDCCProjectCompiler::writeMakeObjFilesRules(QFile &file) { PCppParser parser = mProject->cppParser(); - QString precompileStr; QList projectUnits=mProject->unitList(); foreach(const PProjectUnit &unit, projectUnits) { @@ -233,7 +218,7 @@ void SDCCProjectCompiler::writeMakeObjFilesRules(QFile &file) QString shortFileName = extractRelativePath(mProject->makeFileName(),unit->fileName()); writeln(file); - QString objStr=genMakePath2(shortFileName); + QString objStr = escapeFilenameForMakefilePrerequisite(shortFileName); // if we have scanned it, use scanned info if (parser && parser->fileScanned(unit->fileName())) { QSet fileIncludes = parser->getIncludedFiles(unit->fileName()); @@ -241,34 +226,36 @@ void SDCCProjectCompiler::writeMakeObjFilesRules(QFile &file) if (unit2==unit) continue; if (fileIncludes.contains(unit2->fileName())) { - objStr = objStr + ' ' + genMakePath2(extractRelativePath(mProject->makeFileName(),unit2->fileName())); + QString header = extractRelativePath(mProject->makeFileName(),unit2->fileName()); + objStr = objStr + ' ' + escapeFilenameForMakefilePrerequisite(header); } } } else { foreach(const PProjectUnit &unit2, projectUnits) { FileType fileType = getFileType(unit2->fileName()); - if (fileType == FileType::CHeader || fileType==FileType::CppHeader) - objStr = objStr + ' ' + genMakePath2(extractRelativePath(mProject->makeFileName(),unit2->fileName())); + if (fileType == FileType::CHeader || fileType==FileType::CppHeader) { + QString header = extractRelativePath(mProject->makeFileName(),unit2->fileName()); + objStr = objStr + ' ' + escapeFilenameForMakefilePrerequisite(header); + } } } - QString objFileName; - QString objFileName2; + QString objFileNameTarget; + QString objFileNameCommand; if (!mProject->options().objectOutput.isEmpty()) { QString fullObjname = includeTrailingPathDelimiter(mProject->options().objectOutput) + extractFileName(unit->fileName()); - objFileName = genMakePath2(extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, SDCC_REL_SUFFIX))); - objFileName2 = genMakePath1(extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, SDCC_REL_SUFFIX))); -// if (!extractFileDir(ObjFileName).isEmpty()) { -// objStr = genMakePath2(includeTrailingPathDelimiter(extractFileDir(ObjFileName))) + objStr; -// } + QString objFile = extractRelativePath(mProject->makeFileName(), changeFileExt(fullObjname, SDCC_REL_SUFFIX)); + objFileNameTarget = escapeFilenameForMakefileTarget(objFile); + objFileNameCommand = escapeArgumentForMakefileRecipe(objFile, false); } else { - objFileName = genMakePath2(changeFileExt(shortFileName, SDCC_REL_SUFFIX)); - objFileName2 = genMakePath1(changeFileExt(shortFileName, SDCC_REL_SUFFIX)); + QString objFile = changeFileExt(shortFileName, SDCC_REL_SUFFIX); + objFileNameTarget = escapeFilenameForMakefileTarget(objFile); + objFileNameCommand = escapeArgumentForMakefileRecipe(objFile, false); } - objStr = objFileName + ": "+objStr+precompileStr; + objStr = objFileNameTarget + ": " + objStr; - writeln(file,objStr); + writeln(file, objStr); // Write custom build command if (unit->overrideBuildCmd() && !unit->buildCmd().isEmpty()) { @@ -278,7 +265,7 @@ void SDCCProjectCompiler::writeMakeObjFilesRules(QFile &file) // Or roll our own } else { if (fileType==FileType::CSource) { - writeln(file, "\t$(CC) $(CFLAGS) -c " + genMakePath1(shortFileName)); + writeln(file, "\t$(CC) $(CFLAGS) -c " + escapeArgumentForMakefileRecipe(shortFileName, false)); } } } @@ -323,39 +310,44 @@ bool SDCCProjectCompiler::prepareForCompile() QString parallelParam; if (mProject->options().allowParallelBuilding) { if (mProject->options().parellelBuildingJobs==0) { - parallelParam = " --jobs"; + parallelParam = "--jobs"; } else { - parallelParam = QString(" -j%1").arg(mProject->options().parellelBuildingJobs); + parallelParam = QString("-j%1").arg(mProject->options().parellelBuildingJobs); } + } else { + parallelParam = "-j1"; } + QString makefile = + extractRelativePath(mProject->directory(), mProject->makeFileName()); + QStringList cleanArgs{ + "-f", + makefile, + "clean", + }; + QStringList makeAllArgs{ + parallelParam, + "-f", + makefile, + "all", + }; if (onlyClean()) { - mArguments = QString(" %1 -f \"%2\" clean").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName())); + mArguments = cleanArgs; } else if (mRebuild) { - mArguments = QString(" -f \"%1\" clean").arg(extractRelativePath( - mProject->directory(), - mProject->makeFileName())); - mExtraCompilersList.append(mCompiler); - mExtraOutputFilesList.append(""); - mExtraArgumentsList.append(QString(" %1 -f \"%2\" all").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName()))); + mArguments = cleanArgs; + mExtraCompilersList << mCompiler; + mExtraOutputFilesList << ""; + mExtraArgumentsList << makeAllArgs; } else { - mArguments = QString(" %1 -f \"%2\" all").arg(parallelParam, - extractRelativePath( - mProject->directory(), - mProject->makeFileName())); + mArguments = makeAllArgs; } mDirectory = mProject->directory(); log(tr("Processing makefile:")); log("--------"); log(tr("- makefile processer: %1").arg(mCompiler)); - log(tr("- Command: %1 %2").arg(extractFileName(mCompiler)).arg(mArguments)); + QString command = escapeCommandForLog(mCompiler, mArguments); + log(tr("- Command: %1").arg(command)); log(""); return true; diff --git a/RedPandaIDE/compiler/stdincompiler.cpp b/RedPandaIDE/compiler/stdincompiler.cpp index b025eaf3..ba4ed54f 100644 --- a/RedPandaIDE/compiler/stdincompiler.cpp +++ b/RedPandaIDE/compiler/stdincompiler.cpp @@ -46,7 +46,7 @@ bool StdinCompiler::prepareForCompile() } switch(fileType) { case FileType::CSource: - mArguments += " -x c - "; + mArguments += {"-x", "c", "-"}; mArguments += getCCompileArguments(mOnlyCheckSyntax); mArguments += getCIncludeArguments(); mArguments += getProjectIncludeArguments(); @@ -54,7 +54,7 @@ bool StdinCompiler::prepareForCompile() mCompiler = compilerSet()->CCompiler(); break; case FileType::GAS: - mArguments += " -x assembler - "; + mArguments += {"-x", "assembler", "-"}; mArguments += getCCompileArguments(mOnlyCheckSyntax); mArguments += getCIncludeArguments(); mArguments += getProjectIncludeArguments(); @@ -64,7 +64,7 @@ bool StdinCompiler::prepareForCompile() case FileType::CppSource: case FileType::CppHeader: case FileType::CHeader: - mArguments += " -x c++ - "; + mArguments += {"-x", "c++", "-"}; mArguments += getCppCompileArguments(mOnlyCheckSyntax); mArguments += getCppIncludeArguments(); mArguments += getProjectIncludeArguments(); @@ -87,7 +87,8 @@ bool StdinCompiler::prepareForCompile() log(tr("Processing %1 source file:").arg(strFileType)); log("------------------"); log(tr("%1 Compiler: %2").arg(strFileType).arg(mCompiler)); - log(tr("Command: %1 %2").arg(extractFileName(mCompiler)).arg(mArguments)); + QString command = escapeCommandForLog(mCompiler, mArguments); + log(tr("Command: %1").arg(command)); mDirectory = extractFileDir(mFilename); return true; } diff --git a/RedPandaIDE/debugger.cpp b/RedPandaIDE/debugger.cpp index bf241db9..53ebe45f 100644 --- a/RedPandaIDE/debugger.cpp +++ b/RedPandaIDE/debugger.cpp @@ -16,6 +16,7 @@ */ #include "debugger.h" #include "utils.h" +#include "utils/parsearg.h" #include "mainwindow.h" #include "editor.h" #include "settings.h" @@ -140,7 +141,7 @@ bool Debugger::start(int compilerSetIndex, const QString& inferior, const QStrin //deleted when thread finished QStringList params; if (pSettings->executor().useParams()) - params = splitProcessCommand(pSettings->executor().params()); + params = parseArgumentsWithoutVariables(pSettings->executor().params()); mTarget = new DebugTarget(inferior,compilerSet->debugServer(),pSettings->debugger().GDBServerPort(),params); if (pSettings->executor().redirectInput()) mTarget->setInputFile(pSettings->executor().inputFilename()); diff --git a/RedPandaIDE/main.cpp b/RedPandaIDE/main.cpp index 7898d191..64621501 100644 --- a/RedPandaIDE/main.cpp +++ b/RedPandaIDE/main.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "common.h" #include "colorscheme.h" #include "iconsmanager.h" diff --git a/RedPandaIDE/mainwindow.cpp b/RedPandaIDE/mainwindow.cpp index 9f66b99d..c17285db 100644 --- a/RedPandaIDE/mainwindow.cpp +++ b/RedPandaIDE/mainwindow.cpp @@ -22,6 +22,8 @@ #include "settings.h" #include "qsynedit/constants.h" #include "debugger.h" +#include "utils/escape.h" +#include "utils/parsearg.h" #include "widgets/cpudialog.h" #include "widgets/filepropertiesdialog.h" #include "widgets/filenameeditdelegate.h" @@ -3376,28 +3378,29 @@ void MainWindow::updateTools() QAction* action = new QAction(item->title,ui->menuTools); connect(action, &QAction::triggered, [item] (){ - QString program = parseMacros(item->program); - QString workDir = parseMacros(item->workingDirectory); - QString params = parseMacros(item->parameters); + QMap macros = devCppMacroVariables(); + QString program = parseMacros(item->program, macros); + QString workDir = parseMacros(item->workingDirectory, macros); + QStringList params = parseArguments(item->parameters, macros, true); if (!program.endsWith(".bat",Qt::CaseInsensitive)) { QTemporaryFile file(QDir::tempPath()+QDir::separator()+"XXXXXX.bat"); file.setAutoRemove(false); if (file.open()) { - file.write(QString("cd /d \"%1\"") - .arg(localizePath(workDir)) - .toLocal8Bit()+LINE_BREAKER); - file.write((program+" "+params).toLocal8Bit() + file.write(escapeCommandForPlatformShell( + "cd", {"/d", localizePath(workDir)} + ).toLocal8Bit() + LINE_BREAKER); + file.write(escapeCommandForPlatformShell(program, params).toLocal8Bit() + LINE_BREAKER); file.close(); if (item->pauseAfterExit) { executeFile( includeTrailingPathDelimiter(pSettings->dirs().appLibexecDir())+CONSOLE_PAUSER, - " 1 \""+localizePath(file.fileName())+"\" ", + {"1", localizePath(file.fileName())}, workDir, file.fileName()); } else { executeFile( file.fileName(), - "", + {}, workDir, file.fileName()); } } @@ -3405,7 +3408,7 @@ void MainWindow::updateTools() if (item->pauseAfterExit) { executeFile( includeTrailingPathDelimiter(pSettings->dirs().appLibexecDir())+CONSOLE_PAUSER, - " 1 \""+program+"\" "+params, + QStringList{"1", program} + params, workDir, ""); } else { executeFile( diff --git a/RedPandaIDE/project.cpp b/RedPandaIDE/project.cpp index 94558032..d49d46e9 100644 --- a/RedPandaIDE/project.cpp +++ b/RedPandaIDE/project.cpp @@ -1423,11 +1423,7 @@ void Project::buildPrivateResource() if ( (getFileType(unit->fileName()) == FileType::WindowsResourceSource) && unit->compile() ) - contents.append("#include \"" + - genMakePath( - extractRelativePath(directory(), unit->fileName()), - false, - false) + "\""); + contents.append("#include \"" + extractRelativePath(directory(), unit->fileName()) + "\""); } if (!mOptions.icon.isEmpty()) { @@ -1450,11 +1446,8 @@ void Project::buildPrivateResource() contents.append("//"); if (!mOptions.exeOutput.isEmpty()) contents.append( - "1 24 \"" + - genMakePath2( - includeTrailingPathDelimiter(mOptions.exeOutput) - + extractFileName(executable())) - + ".Manifest\""); + "1 24 \"" + includeTrailingPathDelimiter(mOptions.exeOutput) + + extractFileName(executable()) + ".Manifest\""); else contents.append("1 24 \"" + extractFileName(executable()) + ".Manifest\""); } diff --git a/RedPandaIDE/resources/terminal-unix.json b/RedPandaIDE/resources/terminal-unix.json index e8b3b595..8ce5be29 100644 --- a/RedPandaIDE/resources/terminal-unix.json +++ b/RedPandaIDE/resources/terminal-unix.json @@ -15,8 +15,8 @@ { "name": "CoreTerminal", "path": "coreterminal", - "argsPattern": "$term -e \"$command\"", - "comment": "The pair of quotation mark around `$command` is only a visual symbol, not actually required." + "argsPattern": "$term -e \"$unix_command\"", + "comment": "The pair of quotation mark around `$unix_command` is only a visual symbol, not actually required." }, { "name": "iTerm2", @@ -26,7 +26,7 @@ { "name": "kermit", "path": "kermit", - "argsPattern": "$term -e \"$command\"" + "argsPattern": "$term -e \"$unix_command\"" }, { "name": "Kitty", @@ -36,12 +36,12 @@ { "name": "ROXTerm", "path": "roxterm", - "argsPattern": "$term -e \"$command\"" + "argsPattern": "$term -e \"$unix_command\"" }, { "name": "sakura", "path": "sakura", - "argsPattern": "$term -e \"$command\"" + "argsPattern": "$term -e \"$unix_command\"" }, { "name": "Termit", @@ -51,7 +51,7 @@ { "name": "Termite", "path": "termite", - "argsPattern": "$term -e \"$command\"" + "argsPattern": "$term -e \"$unix_command\"" }, { "name": "Tilix", @@ -116,7 +116,7 @@ { "name": "Terminology (Enlightenment)", "path": "terminology", - "argsPattern": "$term -e \"$command\"" + "argsPattern": "$term -e \"$unix_command\"" }, { "name": "Xfce Terminal", @@ -131,7 +131,7 @@ { "name": "Elementary Terminal", "path": "io.elementary.terminal", - "argsPattern": "$term -e \"$command\"", + "argsPattern": "$term -e \"$unix_command\"", "comment": "confirm to quit" }, { diff --git a/RedPandaIDE/settings.cpp b/RedPandaIDE/settings.cpp index 2c784e35..d54b5a6c 100644 --- a/RedPandaIDE/settings.cpp +++ b/RedPandaIDE/settings.cpp @@ -19,6 +19,8 @@ #include #include #include "utils.h" +#include "utils/escape.h" +#include "utils/parsearg.h" #include #include "systemconsts.h" #include @@ -1934,14 +1936,14 @@ Settings::CompilerSet::CompilerSet(const QJsonObject &set) : mFullLoaded = false; } - QStringList escapedCompileParams; + QStringList compileParams; for (const QJsonValue ¶m : set["customCompileParams"].toArray()) - escapedCompileParams.append(escapeArgument(param.toString(), false)); - mCustomCompileParams = escapedCompileParams.join(' '); - QStringList escapedLinkParams; + compileParams << param.toString(); + mCustomCompileParams = escapeArgumentsForInputField(compileParams); + QStringList linkParams; for (const QJsonValue ¶m : set["customLinkParams"].toArray()) - escapedLinkParams.append(escapeArgument(param.toString(), false)); - mCustomLinkParams = escapedLinkParams.join(' '); + linkParams << param.toString(); + mCustomLinkParams = escapeArgumentsForInputField(linkParams); if (!mAutoAddCharsetParams) mExecCharset = "UTF-8"; @@ -2597,7 +2599,7 @@ QStringList Settings::CompilerSet::defines(bool isCpp) { #endif if (mUseCustomCompileParams) { - QStringList extraParams = splitProcessCommand(mCustomCompileParams); + QStringList extraParams = parseArgumentsWithoutVariables(mCustomCompileParams); arguments.append(extraParams); } arguments.append(NULL_FILE); @@ -4122,7 +4124,12 @@ void Settings::Environment::checkAndSetTerminal() QMessageBox::critical( nullptr, QCoreApplication::translate("Settings","Error"), - QCoreApplication::translate("Settings","Can't find terminal program!")); + QCoreApplication::translate("Settings","Can't find terminal program!")); +} + +QMap Settings::Environment::terminalArgsPatternMagicVariables() +{ + return mTerminalArgsPatternMagicVariables; } QList Settings::Environment::loadTerminalList() const @@ -4165,7 +4172,7 @@ bool Settings::Environment::isTerminalValid() // terminal patter is empty if (mTerminalArgumentsPattern.isEmpty()) return false; - QStringList patternItems = splitProcessCommand(mTerminalArgumentsPattern); + QStringList patternItems = parseArguments(mTerminalArgumentsPattern, mTerminalArgsPatternMagicVariables, false); if (!(patternItems.contains("$argv") || patternItems.contains("$command") @@ -4242,6 +4249,17 @@ void Settings::Environment::setTheme(const QString &theme) mTheme = theme; } +const QMap Settings::Environment::mTerminalArgsPatternMagicVariables = { + {"term", "$term"}, + {"integrated_term", "$integrated_term"}, + {"argv", "$argv"}, + {"command", "$command"}, + {"unix_command", "$unix_command"}, + {"dos_command", "$dos_command"}, + {"lpCommandLine", "$lpCommandLine"}, + {"tmpfile", "$tmpfile"}, +}; + Settings::Executor::Executor(Settings *settings):_Base(settings, SETTING_EXECUTOR) { @@ -6708,21 +6726,39 @@ std::tuple> wrapCommandFor wrappedArgs.append(includeTrailingPathDelimiter(pSettings->dirs().appDir())+terminal); else if (patternItem == "$argv") wrappedArgs.append(payloadArgsWithArgv0); - else if (patternItem == "$command") { + else if (patternItem == "$command" || patternItem == "$unix_command") { + // “$command” is for compatibility; previously used on multiple Unix terms QStringList escapedArgs; for (int i = 0; i < payloadArgsWithArgv0.length(); i++) { auto &arg = payloadArgsWithArgv0[i]; - auto escaped = escapeArgument(arg, i == 0); + auto escaped = escapeArgument(arg, i == 0, EscapeArgumentRule::BourneAgainShellPretty); escapedArgs.append(escaped); } wrappedArgs.push_back(escapedArgs.join(' ')); - } else if (patternItem == "$tmpfile") { + } else if (patternItem == "$dos_command") { + QStringList escapedArgs; + for (int i = 0; i < payloadArgsWithArgv0.length(); i++) { + auto &arg = payloadArgsWithArgv0[i]; + auto escaped = escapeArgument(arg, i == 0, EscapeArgumentRule::WindowsCommandPrompt); + escapedArgs.append(escaped); + } + wrappedArgs.push_back(escapedArgs.join(' ')); + } else if (patternItem == "$lpCommandLine") { + QStringList escapedArgs; + for (int i = 0; i < payloadArgsWithArgv0.length(); i++) { + auto &arg = payloadArgsWithArgv0[i]; + auto escaped = escapeArgument(arg, i == 0, EscapeArgumentRule::WindowsCreateProcess); + escapedArgs.append(escaped); + } + wrappedArgs.push_back(escapedArgs.join(' ')); + } else if (patternItem == "$tmpfile" || patternItem == "$tmpfile.command") { + // “$tmpfile” is for compatibility; previously used on macOS Terminal.app temproryFile = std::make_unique(QDir::tempPath() + "/redpanda_XXXXXX.command"); if (temproryFile->open()) { QStringList escapedArgs; for (int i = 0; i < payloadArgsWithArgv0.length(); i++) { auto &arg = payloadArgsWithArgv0[i]; - auto escaped = escapeArgument(arg, i == 0); + auto escaped = escapeArgument(arg, i == 0, EscapeArgumentRule::BourneAgainShellPretty); escapedArgs.append(escaped); } temproryFile->write(escapedArgs.join(' ').toUtf8()); @@ -6731,6 +6767,21 @@ std::tuple> wrapCommandFor QFile(temproryFile->fileName()).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner); } wrappedArgs.push_back(temproryFile->fileName()); + } else if (patternItem == "$tmpfile.bat") { + temproryFile = std::make_unique(QDir::tempPath() + "/redpanda_XXXXXX.bat"); + if (temproryFile->open()) { + QStringList escapedArgs; + for (int i = 0; i < payloadArgsWithArgv0.length(); i++) { + auto &arg = payloadArgsWithArgv0[i]; + auto escaped = escapeArgument(arg, i == 0, EscapeArgumentRule::WindowsCommandPrompt); + escapedArgs.append(escaped); + } + temproryFile->write(escapedArgs.join(' ').toLocal8Bit()); + temproryFile->write("\r\n"); + temproryFile->flush(); + QFile(temproryFile->fileName()).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner); + } + wrappedArgs.push_back(temproryFile->fileName()); } else wrappedArgs.push_back(patternItem); } @@ -6741,5 +6792,5 @@ std::tuple> wrapCommandFor std::tuple> wrapCommandForTerminalEmulator(const QString &terminal, const QString &argsPattern, const QStringList &payloadArgsWithArgv0) { - return wrapCommandForTerminalEmulator(terminal, splitProcessCommand(argsPattern), payloadArgsWithArgv0); + return wrapCommandForTerminalEmulator(terminal, parseArguments(argsPattern, Settings::Environment::terminalArgsPatternMagicVariables(), false), payloadArgsWithArgv0); } diff --git a/RedPandaIDE/settings.h b/RedPandaIDE/settings.h index f931a27b..f520b429 100644 --- a/RedPandaIDE/settings.h +++ b/RedPandaIDE/settings.h @@ -631,6 +631,8 @@ public: QList loadTerminalList() const; + static QMap terminalArgsPatternMagicVariables(); + private: bool isTerminalValid(); void checkAndSetTerminal(); @@ -653,6 +655,8 @@ public: bool mUseCustomTerminal; bool mHideNonSupportFilesInFileView; bool mOpenFilesInSingleInstance; + + static const QMap mTerminalArgsPatternMagicVariables; // _Base interface protected: void doSave() override; diff --git a/RedPandaIDE/settingsdialog/environmentprogramswidget.cpp b/RedPandaIDE/settingsdialog/environmentprogramswidget.cpp index 9403f5b6..9c2980ac 100644 --- a/RedPandaIDE/settingsdialog/environmentprogramswidget.cpp +++ b/RedPandaIDE/settingsdialog/environmentprogramswidget.cpp @@ -20,6 +20,7 @@ #include "../iconsmanager.h" #include "../systemconsts.h" #include "../compiler/executablerunner.h" +#include "utils/escape.h" #include #include @@ -43,18 +44,16 @@ EnvironmentProgramsWidget::~EnvironmentProgramsWidget() auto EnvironmentProgramsWidget::resolveExecArguments(const QString &terminalPath, const QString &argsPattern) -> std::tuple> { - QString shell = defaultShell(); - QStringList payloadArgs{shell, "-c", "echo hello; sleep 3"}; - return wrapCommandForTerminalEmulator(terminalPath, argsPattern, payloadArgs); + return wrapCommandForTerminalEmulator(terminalPath, argsPattern, platformCommandForTerminalArgsPreview()); } void EnvironmentProgramsWidget::updateCommandPreview(const QString &terminalPath, const QString &argsPattern) { auto [filename, arguments, fileOwner] = resolveExecArguments(terminalPath, argsPattern); for (auto &arg : arguments) - arg = escapeArgument(arg, false); + arg = escapeArgument(arg, false, platformShellEscapeArgumentRule()); - ui->labelCmdPreviewResult->setPlainText(escapeArgument(filename, true) + " " + arguments.join(' ')); + ui->labelCmdPreviewResult->setPlainText(escapeArgument(filename, true, platformShellEscapeArgumentRule()) + " " + arguments.join(' ')); } void EnvironmentProgramsWidget::autoDetectAndUpdateArgumentsPattern(const QString &terminalPath) diff --git a/RedPandaIDE/settingsdialog/executorgeneralwidget.cpp b/RedPandaIDE/settingsdialog/executorgeneralwidget.cpp index 5e05e7cb..95551fb9 100644 --- a/RedPandaIDE/settingsdialog/executorgeneralwidget.cpp +++ b/RedPandaIDE/settingsdialog/executorgeneralwidget.cpp @@ -19,6 +19,7 @@ #include "../settings.h" #include "../iconsmanager.h" #include "../systemconsts.h" +#include "utils/parsearg.h" #include #include @@ -90,7 +91,7 @@ void ExecutorGeneralWidget::updateIcons(const QSize &/*size*/) void ExecutorGeneralWidget::on_txtExecuteParamaters_textChanged(const QString &commandLine) { - QStringList parsed = splitProcessCommand(commandLine); + QStringList parsed = parseArgumentsWithoutVariables(commandLine); QJsonArray obj = QJsonArray::fromStringList(parsed); ui->txtParsedArgsInJson->setText(QJsonDocument{obj}.toJson()); } diff --git a/RedPandaIDE/settingsdialog/projectcompileparamaterswidget.cpp b/RedPandaIDE/settingsdialog/projectcompileparamaterswidget.cpp index 1c5cc41e..bb5e5270 100644 --- a/RedPandaIDE/settingsdialog/projectcompileparamaterswidget.cpp +++ b/RedPandaIDE/settingsdialog/projectcompileparamaterswidget.cpp @@ -19,6 +19,8 @@ #include "../mainwindow.h" #include "../project.h" #include "../iconsmanager.h" +#include "utils.h" +#include "utils/escape.h" #ifdef Q_OS_WIN #include @@ -83,7 +85,7 @@ void ProjectCompileParamatersWidget::on_btnChooseLib_clicked() ); if (!files.isEmpty()) { foreach (const QString& file,files) { - ui->txtLinker->appendPlainText(" "+genMakePath1(file)); + ui->txtLinker->appendPlainText(" " + escapeArgument(file, false, EscapeArgumentRule::BourneAgainShellPretty)); } } } diff --git a/RedPandaIDE/settingsdialog/toolsgeneralwidget.cpp b/RedPandaIDE/settingsdialog/toolsgeneralwidget.cpp index 2b7e5504..24293e0c 100644 --- a/RedPandaIDE/settingsdialog/toolsgeneralwidget.cpp +++ b/RedPandaIDE/settingsdialog/toolsgeneralwidget.cpp @@ -19,6 +19,9 @@ #include "../mainwindow.h" #include "../settings.h" #include "../iconsmanager.h" +#include "utils.h" +#include "utils/escape.h" +#include "utils/parsearg.h" #include #include @@ -132,9 +135,11 @@ void ToolsGeneralWidget::onEdited() void ToolsGeneralWidget::updateDemo() { - ui->txtDemo->setText( - parseMacros(ui->txtProgram->text())+ " " + - parseMacros(ui->txtParameters->text())); + QMap macros = devCppMacroVariables(); + ui->txtDemo->setText(escapeCommandForPlatformShell( + parseMacros(ui->txtProgram->text(), macros), + parseArguments(ui->txtParameters->text(), macros, true) + )); } ToolsModel::ToolsModel(QObject *parent):QAbstractListModel(parent) diff --git a/RedPandaIDE/test/escape.cpp b/RedPandaIDE/test/escape.cpp new file mode 100644 index 00000000..43e9f1cf --- /dev/null +++ b/RedPandaIDE/test/escape.cpp @@ -0,0 +1,157 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "utils/escape.h" + +int testIndex = 0; + +QByteArray content = "main(){}"; + +void testMake(QString name) +{ + ++testIndex; + auto dir = QString("test-escape-%1").arg(testIndex); + auto srcName = name + ".c"; + auto objName = name + ".o"; +#ifdef Q_OS_WIN + auto binName = name + ".exe"; +#else + auto binName = name; +#endif + auto includeMfName = name + ".mf"; + + auto fail = [&name](const QString& msg) { + qDebug() << "Error in test" << testIndex << name << ":" << msg; + exit(1); + }; + + // create directory + { + auto cwd = QDir(); + if (cwd.exists(dir)) { + if (!QDir(dir).removeRecursively()) + fail("cannot remove directory"); + } + if (!cwd.mkdir(dir)) + fail("cannot create directory"); + } + + // create source file + { + auto srcPath = QString("%1/%2").arg(dir, srcName); + QFile f(srcPath); + if (!f.open(QIODevice::WriteOnly)) + fail("cannot create source file"); + if (f.write(content) != content.size()) + fail("cannot write to source file"); + f.close(); + } + + // create included makefile + { + auto file = QString("%1/%2").arg(dir, includeMfName); + QFile f(file); + if (!f.open(QIODevice::WriteOnly)) + fail("cannot create included makefile"); + f.close(); + } + + // create makefile + { + QStringList mf; + mf << "BIN_DEP = " + escapeFilenameForMakefilePrerequisite(binName); + mf << "BIN_TAR = " + escapeFilenameForMakefileTarget(binName); + mf << "BIN_ARG = " + escapeArgumentForMakefileVariableValue(binName, false); + mf << "OBJS_DEP = " + escapeFilenameForMakefilePrerequisite(objName); + mf << "OBJS_ARG = " + escapeArgumentForMakefileVariableValue(objName, false); + mf << "include " + escapeFilenameForMakefileInclude(includeMfName); + mf << ".PHONY: all clean"; + mf << "all: $(BIN_DEP)"; + mf << "$(BIN_TAR): $(OBJS_DEP)"; + mf << "\tgcc -o $(BIN_ARG) $(OBJS_ARG)"; + mf << escapeFilenameForMakefileTarget(objName) + ": " + escapeFilenameForMakefilePrerequisite(srcName); + mf << "\tgcc -o " + escapeArgumentForMakefileRecipe(objName, false) + " -c " + escapeArgumentForMakefileRecipe(srcName, false); + mf << "clean:"; + mf << "\trm -f $(BIN_ARG) $(OBJS_ARG)"; + + auto file = QString("%1/makefile").arg(dir); + QFile f(file); + if (!f.open(QIODevice::WriteOnly)) + fail("cannot create makefile"); + f.write(mf.join("\n").toUtf8()); + } + + // run make + { + QProcess p; + p.setWorkingDirectory(dir); +#ifdef Q_OS_WIN + p.setProgram("mingw32-make.exe"); +#else + p.setProgram("make"); +#endif + p.start(); + p.waitForFinished(); + if (p.exitCode() != 0) { + qDebug() << "Error in test" << testIndex << name << ": make failed"; + exit(1); + } + auto binFile = QString("%1/%2").arg(dir, binName); + if (!QFile(binFile).exists()) { + qDebug() << "Error in test" << testIndex << name << ": executable not properly created"; + exit(1); + } + } +} + +int main() +{ + testMake("simple"); + testMake("dollar$dollar"); + testMake("paren(paren"); + testMake("paren)paren"); + testMake("pair(of)paren"); + testMake("bracket[bracket"); + testMake("bracket]bracket"); + testMake("pair[of]brackets"); + testMake("brace{brace"); + testMake("brace}brace"); + testMake("pair{of}braces"); + testMake("hash#hash"); + testMake("percent%percent"); + testMake("ampersand&ersand"); + testMake("space space"); + testMake("quote'quote"); + testMake("complex$(complex)complex"); + testMake("complex${complex}complex"); + +#ifndef Q_OS_WIN + testMake("colon:colon"); + testMake("lessgreater"); + testMake("pairangle"); + testMake("asterisk*asterisk"); + testMake("question?question"); + testMake("escape\033escape"); + testMake(R"(quote"quote)"); + testMake(R"(backslash\backslash)"); + testMake(R"(complex\#complex)"); + testMake(R"(complex\ complex)"); + testMake(R"(weird\)"); + testMake(R"(weird\\)"); + testMake(R"(weird\\\)"); + testMake(R"(weird\\\\)"); +#endif + + // seems impossible: + // testMake("tab\ttab"); + // testMake("newline\nnewline"); + + return 0; +} diff --git a/RedPandaIDE/utils.cpp b/RedPandaIDE/utils.cpp index afd17f59..5c3368d6 100644 --- a/RedPandaIDE/utils.cpp +++ b/RedPandaIDE/utils.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "editor.h" #include "editorlist.h" #include "settings.h" @@ -23,72 +25,6 @@ using pIsWow64Process2_t = BOOL (WINAPI *)( ); #endif -QStringList splitProcessCommand(const QString &cmd) -{ - QStringList result; - SplitProcessCommandQuoteType quoteType = SplitProcessCommandQuoteType::None; - int i=0; - QString current; - while (i ¯os) { QString result = s; + for (auto it = macros.begin(); it != macros.end(); ++it) { + QString key = it.key(); + QString value = it.value(); + result.replace('<' + key + '>', value); + } + return result; +} + +QMap devCppMacroVariables() +{ Editor *e = pMainWindow->editorList()->getEditor(); - result.replace("", localizePath(QDir::currentPath())); - result.replace("", localizePath(pSettings->dirs().executable())); - result.replace("", REDPANDA_CPP_VERSION); - result.replace("", localizePath(pSettings->dirs().appDir())); - QDate today = QDate::currentDate(); - QDateTime now = QDateTime::currentDateTime(); - - result.replace("", today.toString("yyyy-MM-dd")); - result.replace("", now.toString("yyyy-MM-dd hh:mm:ss")); + QMap result = { + {"DEFAULT", localizePath(QDir::currentPath())}, + {"DEVCPP", localizePath(pSettings->dirs().executable())}, + {"DEVCPPVERSION", REDPANDA_CPP_VERSION}, + {"EXECPATH", localizePath(pSettings->dirs().appDir())}, + {"DATE", QDate::currentDate().toString("yyyy-MM-dd")}, + {"DATETIME", QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")} + }; Settings::PCompilerSet compilerSet = pSettings->compilerSets().defaultSet(); if (compilerSet) { // Only provide the first cpp include dir - if (compilerSet->defaultCppIncludeDirs().count()>0) - result.replace("", localizePath(compilerSet->defaultCppIncludeDirs().front())); + if (compilerSet->defaultCppIncludeDirs().count() > 0) + result["INCLUDE"] = localizePath(compilerSet->defaultCppIncludeDirs().front()); else - result.replace("",""); + result["INCLUDE"] = ""; // Only provide the first lib dir - if (compilerSet->defaultLibDirs().count()>0) - result.replace("", localizePath(compilerSet->defaultLibDirs().front())); + if (compilerSet->defaultLibDirs().count() > 0) + result["LIB"] = localizePath(compilerSet->defaultLibDirs().front()); else - result.replace("",""); + result["LIB"] = ""; } - if (e!=nullptr && !e->inProject()) { // Non-project editor macros + if (e != nullptr && !e->inProject()) { // Non-project editor macros QString exeSuffix; Settings::PCompilerSet compilerSet = pSettings->compilerSets().defaultSet(); if (compilerSet) { @@ -266,40 +191,41 @@ QString parseMacros(const QString &s) } else { exeSuffix = DEFAULT_EXECUTABLE_SUFFIX; } - result.replace("", extractFileName(changeFileExt(e->filename(), exeSuffix))); - result.replace("", localizePath(changeFileExt(e->filename(), exeSuffix))); - result.replace("", extractFileName(e->filename())); - result.replace("", localizePath(e->filename())); - result.replace("", extractFileName(e->filename())); - result.replace("", localizePath(extractFileDir(e->filename()))); + result["EXENAME"] = extractFileName(changeFileExt(e->filename(), exeSuffix)); + result["EXEFILE"] = localizePath(changeFileExt(e->filename(), exeSuffix)); + result["PROJECTNAME"] = extractFileName(e->filename()); + result["PROJECTFILE"] = localizePath(e->filename()); + result["PROJECTFILENAME"] = extractFileName(e->filename()); + result["PROJECTPATH"] = localizePath(extractFileDir(e->filename())); } else if (pMainWindow->project()) { - result.replace("", extractFileName(pMainWindow->project()->executable())); - result.replace("", localizePath(pMainWindow->project()->executable())); - result.replace("", pMainWindow->project()->name()); - result.replace("", localizePath(pMainWindow->project()->filename())); - result.replace("", extractFileName(pMainWindow->project()->filename())); - result.replace("", localizePath(pMainWindow->project()->directory())); + result["EXENAME"] = extractFileName(pMainWindow->project()->executable()); + result["EXEFILE"] = localizePath(pMainWindow->project()->executable()); + result["PROJECTNAME"] = pMainWindow->project()->name(); + result["PROJECTFILE"] = localizePath(pMainWindow->project()->filename()); + result["PROJECTFILENAME"] = extractFileName(pMainWindow->project()->filename()); + result["PROJECTPATH"] = localizePath(pMainWindow->project()->directory()); } else { - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); + result["EXENAME"] = ""; + result["EXEFILE"] = ""; + result["PROJECTNAME"] = ""; + result["PROJECTFILE"] = ""; + result["PROJECTFILENAME"] = ""; + result["PROJECTPATH"] = ""; } // Editor macros - if (e!=nullptr) { - result.replace("", extractFileName(e->filename())); - result.replace("", localizePath(e->filename())); - result.replace("", localizePath(extractFileDir(e->filename()))); - result.replace("", e->wordAtCursor()); + if (e != nullptr) { + result["SOURCENAME"] = extractFileName(e->filename()); + result["SOURCEFILE"] = localizePath(e->filename()); + result["SOURCEPATH"] = localizePath(extractFileDir(e->filename())); + result["WORDXY"] = e->wordAtCursor(); } else { - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); - result.replace("", ""); + result["SOURCENAME"] = ""; + result["SOURCEFILE"] = ""; + result["SOURCEPATH"] = ""; + result["WORDXY"] = ""; } + return result; } @@ -447,11 +373,11 @@ QByteArray runAndGetOutput(const QString &cmd, const QString& workingDir, const return result; } -void executeFile(const QString &fileName, const QString ¶ms, const QString &workingDir, const QString &tempFile) +void executeFile(const QString &fileName, const QStringList ¶ms, const QString &workingDir, const QString &tempFile) { ExecutableRunner* runner=new ExecutableRunner( fileName, - splitProcessCommand(params), + params, workingDir); runner->connect(runner, &QThread::finished, [runner,tempFile](){ @@ -630,125 +556,16 @@ QStringList getExecutableSearchPaths() #endif } -QString escapeArgument(const QString &arg, [[maybe_unused]] bool isFirstArg) -{ - auto argContainsOneOf = [&arg](auto... ch) { return (arg.contains(ch) || ...); }; - -#ifdef Q_OS_WINDOWS - // See https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess , - // and https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way . - - // TODO: investigate whether we need escaping for cmd. - - if (!arg.isEmpty() && !argContainsOneOf(' ', '\t', '\n', '\v', '"')) - return arg; - - QString result = "\""; - for (auto it = arg.begin(); ; ++it) { - int nBackslash = 0; - while (it != arg.end() && *it == '\\') { - ++it; - ++nBackslash; - } - if (it == arg.end()) { - // Escape all backslashes, but let the terminating double quotation mark we add below be interpreted as a metacharacter. - result.append(QString('\\').repeated(nBackslash * 2)); - break; - } else if (*it == '"') { - // Escape all backslashes and the following double quotation mark. - result.append(QString('\\').repeated(nBackslash * 2 + 1)); - result.push_back(*it); - } else { - // Backslashes aren't special here. - result.append(QString('\\').repeated(nBackslash)); - result.push_back(*it); - } - } - result.push_back('"'); - return result; -#else - /* be speculative, but keep readability. - * ref. https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html - */ - if (arg.isEmpty()) - return R"("")"; - - /* POSIX say the following reserved words (may) have special meaning: - * !, {, }, case, do, done, elif, else, esac, fi, for, if, in, then, until, while, - * [[, ]], function, select, - * only if used as the _first word_ of a command (or somewhere we dot not care). - */ - const static QSet reservedWord{ - "!", "{", "}", "case", "do", "done", "elif", "else", "esac", "fi", "for", "if", "in", "then", "until", "while", - "[[", "]]", "function", "select", - }; - if (isFirstArg && reservedWord.contains(arg)) - return QString(R"("%1")").arg(arg); - - /* POSIX say “shall quote”: - * '|', '&', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\'', ' ', '\t', '\n'; - * and “may need to be quoted”: - * '*', '?', '[', '#', '~', '=', '%'. - * among which “may need to be quoted” there are 4 kinds: - * - wildcards '*', '?', '[' are “danger anywhere” (handle it as if “shall quote”); - * - comment '#', home '~', is “danger at first char in any word”; - * - (environment) variable '=' is “danger at any char in first word”; - * - foreground '%' is “danger at first char in first word”. - * although not mentioned by POSIX, bash’s brace expansion '{', '}' are also “danger anywhere”. - */ - bool isDoubleQuotingDanger = argContainsOneOf('$', '`', '\\', '"'); - bool isSingleQuotingDanger = arg.contains('\''); - bool isDangerAnyChar = isDoubleQuotingDanger || isSingleQuotingDanger || argContainsOneOf( - '|', '&', ';', '<', '>', '(', ')', ' ', '\t', '\n', - '*', '?', '[', - '{', '}' - ); - bool isDangerFirstChar = (arg[0] == '#') || (arg[0] == '~'); - if (isFirstArg) { - isDangerAnyChar = isDangerAnyChar || arg.contains('='); - isDangerFirstChar = isDangerFirstChar || (arg[0] == '%'); - } - - // a “safe” string - if (!isDangerAnyChar && !isDangerFirstChar) - return arg; - - // prefer more-common double quoting - if (!isDoubleQuotingDanger) - return QString(R"("%1")").arg(arg); - - // and then check the opportunity of single quoting - if (!isSingleQuotingDanger) - return QString("'%1'").arg(arg); - - // escaping is necessary - // use double quoting since it’s less tricky - QString result = "\""; - for (auto ch : arg) { - if (ch == '$' || ch == '`' || ch == '\\' || ch == '"') - result.push_back('\\'); - result.push_back(ch); - } - result.push_back('"'); - return result; - - /* single quoting, which is roughly raw string, is possible and quite simple in programming: - * 1. replace each single quote with `'\''`, which contains - * - a single quote to close quoting, - * - an escaped single quote representing the single quote itself, and - * - a single quote to open quoting again; - * 2. enclose the string with a pair of single quotes. - * e.g. `o'clock` => `'o'\''clock'`, really tricky and hard to read. - */ -#endif -} - -QString defaultShell() +QStringList platformCommandForTerminalArgsPreview() { #ifdef Q_OS_WINDOWS - return "powershell.exe"; + QVersionNumber currentVersion = QVersionNumber::fromString(QSysInfo::kernelVersion()); + if (currentVersion >= QVersionNumber(6, 1)) + return {"powershell.exe", "-c", "echo hello; sleep 3"}; + else + return {"cmd.exe", "/c", "echo hello & ping 127.0.0.1"}; #else - return "sh"; + return {"sh", "-c", "echo hello; sleep 3"}; #endif } diff --git a/RedPandaIDE/utils.h b/RedPandaIDE/utils.h index bd13564c..0f351862 100644 --- a/RedPandaIDE/utils.h +++ b/RedPandaIDE/utils.h @@ -116,14 +116,12 @@ enum class ProblemCaseValidateType { }; FileType getFileType(const QString& filename); -QStringList splitProcessCommand(const QString& cmd); -QString genMakePath(const QString& fileName,bool escapeSpaces, bool encloseInQuotes); -QString genMakePath1(const QString& fileName); -QString genMakePath2(const QString& fileName); bool programHasConsole(const QString& filename); QString parseMacros(const QString& s); +QString parseMacros(const QString& s, const QMap& variables); +QMap devCppMacroVariables(); class CppParser; void resetCppParser(std::shared_ptr parser, int compilerSetIndex=-1); @@ -138,7 +136,7 @@ QByteArray runAndGetOutput(const QString& cmd, const QString& workingDir, const void openFileFolderInExplorer(const QString& path); void executeFile(const QString& fileName, - const QString& params, + const QStringList& params, const QString& workingDir, const QString& tempFile); @@ -169,9 +167,7 @@ QColor alphaBlend(const QColor &lower, const QColor &upper); QStringList getExecutableSearchPaths(); -QString escapeArgument(const QString &arg, bool isFirstArg); - -QString defaultShell(); +QStringList platformCommandForTerminalArgsPreview(); QString appArch(); QString osArch(); diff --git a/RedPandaIDE/utils/escape.cpp b/RedPandaIDE/utils/escape.cpp new file mode 100644 index 00000000..24be2c0a --- /dev/null +++ b/RedPandaIDE/utils/escape.cpp @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils/escape.h" + +#include +#include + +#ifdef _MSC_VER +#define __builtin_unreachable() (__assume(0)) +#endif + +static QString contextualBackslashEscaping(const QString &arg, const QSet &needsEscaping, bool escapeFinal = true) +{ + QString result; + for (auto it = arg.begin(); ; ++it) { + int nBackSlash = 0; + while (it != arg.end() && *it == '\\') { + ++it; + ++nBackSlash; + } + if (it == arg.end()) { + if (escapeFinal) { + // escape all backslashes, but leave following character unescaped + // (terminating double quote for CreateProcess, or LF or space in makefile) + result.append(QString('\\').repeated(nBackSlash * 2)); + } else { + // leave all backslashes unescaped, and add a space to protect LF + result.append(QString('\\').repeated(nBackSlash)); + if (nBackSlash > 0) + result.push_back(' '); + } + break; + } else if (needsEscaping.contains(*it)) { + // escape all backslashes and the following character + result.append(QString('\\').repeated(nBackSlash * 2 + 1)); + result.push_back(*it); + } else { + // backslashes aren't special here + result.append(QString('\\').repeated(nBackSlash)); + result.push_back(*it); + } + } + return result; +} + +QString escapeArgumentImplBourneAgainShellPretty(const QString &arg, bool isFirstArg) +{ + // ref. https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html + + if (arg.isEmpty()) + return R"("")"; + + /* POSIX say the following reserved words (may) have special meaning: + * !, {, }, case, do, done, elif, else, esac, fi, for, if, in, then, until, while, + * [[, ]], function, select, + * only if used as the _first word_ of a command (or somewhere we dot not care). + */ + const static QSet reservedWord{ + "!", "{", "}", "case", "do", "done", "elif", "else", "esac", "fi", "for", "if", "in", "then", "until", "while", + "[[", "]]", "function", "select", + }; + if (isFirstArg && reservedWord.contains(arg)) + return QString(R"("%1")").arg(arg); + + /* POSIX say “shall quote”: + * '|', '&', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\'', ' ', '\t', '\n'; + * and “may need to be quoted”: + * '*', '?', '[', '#', '~', '=', '%'. + * among which “may need to be quoted” there are 4 kinds: + * - wildcards '*', '?', '[' are “danger anywhere” (handle it as if “shall quote”); + * - comment '#', home '~', is “danger at first char in any word”; + * - (environment) variable '=' is “danger at any char in first word”; + * - foreground '%' is “danger at first char in first word”. + * although not mentioned by POSIX, bash’s brace expansion '{', '}' are also “danger anywhere”. + */ + + static QRegularExpression doubleQuotingDangerChars(R"([`$\\"])"); + static QRegularExpression otherDangerChars(R"([|&;<>() \t\n*?\[\{\}])"); + bool isDoubleQuotingDanger = arg.contains(doubleQuotingDangerChars); + bool isSingleQuotingDanger = arg.contains('\''); + bool isDangerAnyChar = isDoubleQuotingDanger || isSingleQuotingDanger || arg.contains(otherDangerChars); + bool isDangerFirstChar = (arg[0] == '#') || (arg[0] == '~'); + if (isFirstArg) { + isDangerAnyChar = isDangerAnyChar || arg.contains('='); + isDangerFirstChar = isDangerFirstChar || (arg[0] == '%'); + } + + // a “safe” string + if (!isDangerAnyChar && !isDangerFirstChar) + return arg; + + // prefer more-common double quoting + if (!isDoubleQuotingDanger) + return QString(R"("%1")").arg(arg); + + // and then check the opportunity of single quoting + if (!isSingleQuotingDanger) + return QString("'%1'").arg(arg); + + // escaping is necessary + // use double quoting since it’s less tricky + QString result = "\""; + for (auto ch : arg) { + if (ch == '$' || ch == '`' || ch == '\\' || ch == '"') + result.push_back('\\'); + result.push_back(ch); + } + result.push_back('"'); + return result; +} + +QString escapeArgumentImplBourneAgainShellFast(QString arg) +{ + /* 1. replace each single quote with `'\''`, which contains + * - a single quote to close quoting, + * - an escaped single quote representing the single quote itself, and + * - a single quote to open quoting again. */ + arg.replace('\'', R"('\'')"); + /* 2. enclose the string with a pair of single quotes. */ + return '\'' + arg + '\''; +} + +QString escapeArgumentImplWindowsCreateProcess(const QString &arg, bool forceQuote) +{ + // See https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess , + // and https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way . + + static QRegularExpression needQuoting(R"([ \t\n\v"])"); + if (!arg.isEmpty() && !forceQuote && !arg.contains(needQuoting)) + return arg; + + return '"' + contextualBackslashEscaping(arg, {'"'}) + '"'; +} + +QString escapeArgumentImplWindowsCommandPrompt(const QString &arg) +{ + static QRegularExpression metaChars(R"([()%!^"<>&|])"); + bool containsMeta = arg.contains(metaChars); + if (containsMeta) { + QString quoted = escapeArgumentImplWindowsCreateProcess(arg, false); + quoted.replace('^', "^^"); // handle itself first + quoted.replace('(', "^("); + quoted.replace(')', "^)"); + quoted.replace('%', "^%"); + quoted.replace('!', "^!"); + quoted.replace('"', "^\""); + quoted.replace('<', "^<"); + quoted.replace('>', "^>"); + quoted.replace('&', "^&"); + quoted.replace('|', "^|"); + return quoted; + } else + return escapeArgumentImplWindowsCreateProcess(arg, false); +} + +QString escapeArgument(const QString &arg, bool isFirstArg, EscapeArgumentRule rule) +{ + switch (rule) { + case EscapeArgumentRule::BourneAgainShellPretty: + return escapeArgumentImplBourneAgainShellPretty(arg, isFirstArg); + case EscapeArgumentRule::BourneAgainShellFast: + return escapeArgumentImplBourneAgainShellFast(arg); + case EscapeArgumentRule::WindowsCreateProcess: + return escapeArgumentImplWindowsCreateProcess(arg, false); + case EscapeArgumentRule::WindowsCreateProcessForceQuote: + return escapeArgumentImplWindowsCreateProcess(arg, true); + case EscapeArgumentRule::WindowsCommandPrompt: + return escapeArgumentImplWindowsCommandPrompt(arg); + default: + __builtin_unreachable(); + } +} + +EscapeArgumentRule platformShellEscapeArgumentRule() +{ +#ifdef Q_OS_WIN + return EscapeArgumentRule::WindowsCommandPrompt; +#else + return EscapeArgumentRule::BourneAgainShellPretty; +#endif +} + +QString escapeArgumentForPlatformShell(const QString &arg, bool isFirstArg) +{ + return escapeArgument(arg, isFirstArg, platformShellEscapeArgumentRule()); +} + +QString escapeCommandForPlatformShell(const QString &prog, const QStringList &args) +{ + QStringList escapedArgs{escapeArgumentForPlatformShell(prog, true)}; + for (int i = 0; i < args.size(); ++i) + escapedArgs << escapeArgumentForPlatformShell(args[i], false); + return escapedArgs.join(' '); +} + +EscapeArgumentRule makefileRecipeEscapeArgumentRule() +{ +#ifdef Q_OS_WIN + /* Lord knows why. + + standard CreateProcess or CMD escaping: + child.exe -c main'.c -o main'.o + yielding: + 0: [child.exe] + 1: [-c] + 2: [main.c -o main.o] + that's not what we want. + + however, if CMD escaping a malformed argument + child.exe -c main'.c -o main'.o ^"mal \^" ^& calc^" + yielding: + 0: [child.exe] + 1: [-c] + 2: [main'.c] + 3: [-o] + 4: [main'.o] + 5: [mal " & calc] + it works?!?!?! + + force-quoted CreateProcess escaping seems work on most cases. + */ + return EscapeArgumentRule::WindowsCreateProcessForceQuote; +#else + return EscapeArgumentRule::BourneAgainShellPretty; +#endif +} + +QString escapeArgumentForMakefileVariableValue(const QString &arg, bool isFirstArg) +{ + static QSet needsMfEscaping = {'#'}; + QString recipeEscaped = escapeArgument(arg, isFirstArg, makefileRecipeEscapeArgumentRule()); + QString mfEscaped = contextualBackslashEscaping(recipeEscaped, needsMfEscaping); + return mfEscaped.replace('$', "$$"); +} + +QString escapeArgumentsForMakefileVariableValue(const QStringList &args) +{ + QStringList escapedArgs; + for (int i = 0; i < args.size(); ++i) + escapedArgs << escapeArgumentForMakefileVariableValue(args[i], false); + return escapedArgs.join(' '); +} + +QString escapeFilenameForMakefileInclude(const QString &filename) +{ + static QSet needsEscaping{'#', ' '}; + QString result = contextualBackslashEscaping(filename, needsEscaping); + return result.replace('$', "$$"); +} + +QString escapeFilenameForMakefileTarget(const QString &filename) +{ + static QSet needsEscaping{'#', ' ', ':', '%'}; + QString result = contextualBackslashEscaping(filename, needsEscaping); + return result.replace('$', "$$"); +} + +QString escapeFilenameForMakefilePrerequisite(const QString &filename) +{ + static QSet needsEscaping{'#', ' ', ':', '?', '*'}; + QString result = contextualBackslashEscaping(filename, needsEscaping, false); + return result.replace('$', "$$"); +} + +QString escapeFilenamesForMakefilePrerequisite(const QStringList &filenames) +{ + QStringList escapedFilenames; + for (int i = 0; i < filenames.size(); ++i) + escapedFilenames << escapeFilenameForMakefilePrerequisite(filenames[i]); + return escapedFilenames.join(' '); +} + +QString escapeArgumentForMakefileRecipe(const QString &arg, bool isFirstArg) +{ + QString shellEscaped = escapeArgument(arg, isFirstArg, makefileRecipeEscapeArgumentRule()); + return shellEscaped.replace('$', "$$"); +} + +QString escapeArgumentForInputField(const QString &arg, bool isFirstArg) +{ + return escapeArgument(arg, isFirstArg, EscapeArgumentRule::BourneAgainShellPretty); +} + +QString escapeArgumentsForInputField(const QStringList &args) +{ + QStringList escapedArgs; + for (int i = 0; i < args.size(); ++i) + escapedArgs << escapeArgumentForInputField(args[i], false); + return escapedArgs.join(' '); +} diff --git a/RedPandaIDE/utils/escape.h b/RedPandaIDE/utils/escape.h new file mode 100644 index 00000000..bc266901 --- /dev/null +++ b/RedPandaIDE/utils/escape.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESCAPE_H +#define ESCAPE_H + +#include + +enum class EscapeArgumentRule { + BourneAgainShellPretty, + BourneAgainShellFast, + WindowsCreateProcess, + WindowsCreateProcessForceQuote, + WindowsCommandPrompt, +}; + +QString escapeArgument(const QString &arg, bool isFirstArg, EscapeArgumentRule rule); + +EscapeArgumentRule platformShellEscapeArgumentRule(); +QString escapeArgumentForPlatformShell(const QString &arg, bool isFirstArg); +QString escapeCommandForPlatformShell(const QString &prog, const QStringList &args); + +EscapeArgumentRule makefileRecipeEscapeArgumentRule(); +QString escapeArgumentForMakefileVariableValue(const QString &arg, bool isFirstArg); +QString escapeArgumentsForMakefileVariableValue(const QStringList &args); +QString escapeFilenameForMakefileInclude(const QString &filename); +QString escapeFilenameForMakefileTarget(const QString &filename); +QString escapeFilenameForMakefilePrerequisite(const QString &filename); +QString escapeFilenamesForMakefilePrerequisite(const QStringList &filenames); +QString escapeArgumentForMakefileRecipe(const QString &arg, bool isFirstArg); + +QString escapeArgumentForInputField(const QString &arg, bool isFirstArg); +QString escapeArgumentsForInputField(const QStringList &args); + +#endif // ESCAPE_H diff --git a/RedPandaIDE/utils/parsearg.cpp b/RedPandaIDE/utils/parsearg.cpp new file mode 100644 index 00000000..867e2023 --- /dev/null +++ b/RedPandaIDE/utils/parsearg.cpp @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "parsearg.h" + +namespace ParseArgumentsDetail +{ + +/*Before: + 'blahblah' + ^pos + After: + 'blahblah' + ^pos */ +QString singleQuoted(const QString &command, int &pos) +{ + QString result; + while (pos < command.length() && command[pos] != '\'') { + result.push_back(command[pos]); + ++pos; + } + if (pos < command.length()) + ++pos; // eat closing quote + return result; +} + +// read up to 3 octal digits +QString readOctal(const QString &command, int &pos) +{ + QString result; + for (int i = 0; i < 3; ++i) { + if (pos < command.length() && command[pos] >= '0' && command[pos] <= '7') { + result.push_back(command[pos]); + ++pos; + } else + break; + } + return result; +} + +// read up to maxDigits hex digits +QString readHex(const QString &command, int &pos, int maxDigits) +{ + QString result; + for (int i = 0; i < maxDigits; ++i) { + if (pos < command.length() && (command[pos].isDigit() || (command[pos].toLower() >= 'a' && command[pos].toLower() <= 'f'))) { + result.push_back(command[pos]); + ++pos; + } else + break; + } + return result; +} + +/*Case 1: braced variable name (ingore POSIX operators, no nested braces) + Before: + ${VARNAME.abc$} + ^pos + After: + ${VARNAME.abc$} + ^pos + Returns: value of `VARNAME.abc$`, "" if not found + Case 2: command or arithmetic (no expansion, nested parentheses ok) + Before: + $(echo 1) + ^pos + $((1+1)) + ^pos + After: + $(echo 1) + ^pos + $((1+1)) + ^pos + Returns: as is + Case 3: ANSI-C quoting + Before: + $'blah\nblah\'blah' + ^pos + After: + $'blah\nblah\'blah' + ^pos + Returns: unescaped string + Case 4: normal variable name + Before: + $VAR_NAME-x + ^pos + After: + $VAR_NAME-x + ^pos + Returns: value of `VAR_NAME`, "" if not found + Case 5: all other invalid cases (though they may be valid in shell) + Before: + $123 + ^pos + After: + $123 + ^pos + Returns: as is */ +QString variableExpansion(const QString &command, int &pos, const QMap &variables, bool ansiCQuotingPermitted) +{ + if (pos >= command.length()) + return "$"; + if (command[pos] == '{') { + // case 1, read to closing brace + QString varName; + QString result; + ++pos; // eat opening brace + while (pos < command.length() && command[pos] != '}') { + varName.push_back(command[pos]); + ++pos; + } + if (pos < command.length()) { + ++pos; // eat closing brace + if (variables.contains(varName)) + return variables[varName]; + else + return {}; + } else { + // unterminated + return {}; + } + } else if (command[pos] == '(') { + // case 2, read to matching closing paren + QString result = "$("; + ++pos; // eat opening paren + int level = 1; + while (pos < command.length() && level > 0) { + if (command[pos] == '(') + ++level; + else if (command[pos] == ')') + --level; + result.push_back(command[pos]); + ++pos; + } + return result; + } else if (ansiCQuotingPermitted && command[pos] == '\'') { + // case 3, parse ANSI-C quoting + QByteArray unescaped; + ++pos; // eat opening quote + while (pos < command.length()) { + if (command[pos] == '\\') { + ++pos; + if (pos < command.length()) { + switch (command[pos].unicode()) { + case 'a': + ++pos; + unescaped.push_back('\a'); + break; + case 'b': + ++pos; + unescaped.push_back('\b'); + break; + case 'e': + case 'E': + ++pos; + unescaped.push_back('\x1B'); + break; + case 'f': + ++pos; + unescaped.push_back('\f'); + break; + case 'n': + ++pos; + unescaped.push_back('\n'); + break; + case 'r': + ++pos; + unescaped.push_back('\r'); + break; + case 't': + ++pos; + unescaped.push_back('\t'); + break; + case 'v': + ++pos; + unescaped.push_back('\v'); + break; + case '\\': + ++pos; + unescaped.push_back('\\'); + break; + case '\'': + ++pos; + unescaped.push_back('\''); + break; + case '"': + ++pos; + unescaped.push_back('"'); + break; + case '?': + ++pos; + unescaped.push_back('?'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + QString digits = readOctal(command, pos); + int octal = digits.toUInt(nullptr, 8); + unescaped.push_back(octal); + break; + } + case 'x': { + ++pos; // eat 'x' + QString digits = readHex(command, pos, 2); + if (digits.isEmpty()) { + // normal character + unescaped.append("\\x"); + } else { + int hex = digits.toUInt(nullptr, 16); + unescaped.push_back(hex); + } + break; + } + case 'u': { + ++pos; // eat 'u' + QString digits = readHex(command, pos, 4); + if (digits.isEmpty()) { + // normal character + unescaped.append("\\u"); + } else { + int hex = digits.toUInt(nullptr, 16); + QByteArray encoded = QString(hex).toUtf8(); + unescaped.append(encoded); + } + break; + } + case 'U': { + ++pos; // eat 'U' + QString digits = readHex(command, pos, 8); + if (digits.isEmpty()) { + // normal character + unescaped.append("\\U"); + } else { + int hex = digits.toUInt(nullptr, 16); + QByteArray encoded = QString(hex).toUtf8(); + unescaped.append(encoded); + } + break; + } + default: + // normal character + unescaped.push_back('\\'); + break; + } + } + } else if (command[pos] == '\'') { + ++pos; // eat closing quote + return unescaped; + } else { + QChar c = command[pos]; + QByteArray encoded = QString(c).toUtf8(); + unescaped.append(encoded); + ++pos; + } + } + // unterminated + return unescaped; + } else if (command[pos].isLetter() || command[pos] == '_') { + // case 4, read variable name + QString varName; + while (pos < command.length() && (command[pos].isLetterOrNumber() || command[pos] == '_')) { + varName.push_back(command[pos]); + ++pos; + } + if (variables.contains(varName)) + return variables[varName]; + else + return {}; + } else { + // case 5, return as is + return "$"; + } +} + +/*Before: + + ^pos + After: + + ^pos */ +QString devCppExpansion(const QString &command, int &pos, const QMap &variables) +{ + QString varName; + QString result; + while (pos < command.length() && command[pos] != '>') { + varName.push_back(command[pos]); + ++pos; + } + if (pos < command.length()) { + ++pos; // eat closing bracket + if (variables.contains(varName)) + return variables[varName]; + else + // not a variable + return '<' + varName + '>'; + } else { + // unterminated, treat it as a normal string + return '<' + varName; + } +} + +/*Before: + "blah\"blah" + ^pos + After: + "blah\"blah" + ^pos */ +QString doubleQuoted(const QString &command, int &pos, const QMap &variables, bool enableDevCppVariableExpansion) +{ + QString result; + while (pos < command.length()) { + switch (command[pos].unicode()) { + case '$': + ++pos; // eat '$' + result += variableExpansion(command, pos, variables, false); + break; + case '\\': + ++pos; // eat backslash + if (pos < command.length()) { + switch (command[pos].unicode()) { + case '$': + case '`': + case '"': + case '\\': + result.push_back(command[pos]); + ++pos; + break; + case '\n': + ++pos; // eat newline + break; + default: + // normal character + result.push_back('\\'); + } + } else { + // unterminated + result.push_back('\\'); + } + break; + case '"': + ++pos; // eat closing quote + return result; + case '<': + if (enableDevCppVariableExpansion) { + ++pos; // eat '<' + result += devCppExpansion(command, pos, variables); + break; + } + [[fallthrough]]; + case '`': // not supported + default: + result.push_back(command[pos]); + ++pos; + } + } + // unterminated + return result; +} + +} // namespace ParseArgumentsDetail + +QStringList parseArguments(const QString &command, const QMap &variables, bool enableDevCppVariableExpansion) +{ + using namespace ParseArgumentsDetail; + + QStringList result; + QString current; + bool currentPolluted = false; + + int pos = 0; + while (pos < command.length()) { + switch (command[pos].unicode()) { + case ' ': + case '\t': + case '\n': + if (currentPolluted) { + result.push_back(current); + current.clear(); + currentPolluted = false; + } + ++pos; + break; + case '#': + if (currentPolluted) { + // normal character + current.push_back(command[pos]); + ++pos; + } else { + // comment, eat to newline + while (pos < command.length() && command[pos] != '\n') + ++pos; + } + break; + case '\'': + ++pos; // eat opening quote + current += singleQuoted(command, pos); + currentPolluted = true; + break; + case '"': + ++pos; // eat opening quote + current += doubleQuoted(command, pos, variables, enableDevCppVariableExpansion); + currentPolluted = true; + break; + case '$': + ++pos; // eat '$' + current += variableExpansion(command, pos, variables, true); + currentPolluted = true; + break; + case '\\': + ++pos; // eat backslash + if (pos < command.length()) { + if (command[pos] != '\n') + ++pos; // eat newline + else { + // normal character + current.push_back(command[pos]); + } + ++pos; + currentPolluted = true; + } else { + // unterminated + current.push_back('\\'); + } + break; + case '<': + if (enableDevCppVariableExpansion) { + ++pos; // eat '<' + current += devCppExpansion(command, pos, variables); + currentPolluted = true; + break; + } + [[fallthrough]]; + default: + current.push_back(command[pos]); + ++pos; + currentPolluted = true; + } + } + + if (currentPolluted) + result.push_back(current); + + return result; +} + +QStringList parseArgumentsWithoutVariables(const QString &command) +{ + return parseArguments(command, {}, false); +} diff --git a/RedPandaIDE/utils/parsearg.h b/RedPandaIDE/utils/parsearg.h new file mode 100644 index 00000000..7d31831f --- /dev/null +++ b/RedPandaIDE/utils/parsearg.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef PARSEARG_H +#define PARSEARG_H + +#include +#include + +QStringList parseArguments(const QString &command, const QMap &variables, bool enableDevCppVariableExpansion); +QStringList parseArgumentsWithoutVariables(const QString &command); + +#endif // PARSEARG_H diff --git a/RedPandaIDE/xmake.lua b/RedPandaIDE/xmake.lua index c9115584..c872f085 100644 --- a/RedPandaIDE/xmake.lua +++ b/RedPandaIDE/xmake.lua @@ -47,7 +47,9 @@ target("RedPandaIDE") -- problems "problems/freeprojectsetformat.cpp", "problems/ojproblemset.cpp", - "problems/problemcasevalidator.cpp") + "problems/problemcasevalidator.cpp", + "utils/escape.cpp", + "utils/parsearg.cpp") add_moc_classes( "caretlist", @@ -236,3 +238,13 @@ target("RedPandaIDE") if is_xdg() then on_install(install_bin) end + +target("test-escape") + set_kind("binary") + add_rules("qt.console") + + set_default(false) + add_tests("test-escape") + + add_files("utils/escape.cpp", "test/escape.cpp") + add_includedirs(".") diff --git a/libs/redpanda_qt_utils/qt_utils/utils.cpp b/libs/redpanda_qt_utils/qt_utils/utils.cpp index 261d5b19..144f62b1 100644 --- a/libs/redpanda_qt_utils/qt_utils/utils.cpp +++ b/libs/redpanda_qt_utils/qt_utils/utils.cpp @@ -539,7 +539,7 @@ QString changeFileExt(const QString& filename, QString ext) path = includeTrailingPathDelimiter(fileInfo.path()); } if (suffix.isEmpty()) { - return path+filename+ext; + return path+name+ext; } else { return path+fileInfo.completeBaseName()+ext; }