获取进程的argc和argv的另一种方法

28

我正在寻找一种替代方法,可以获取进程提供的命令行参数argcargv,而不必直接访问传递给main()的变量。

我想创建一个与main()无关的类,这样使用它们的代码就不必显式地传递argcargv了。

编辑:需要澄清一些问题。我有这个类。

class Application
{
  int const argc_;
  char const** const argv_;

public:
  explicit Application(int, char const*[]);
};

Application::Application(int const argc, char const* argv[]) :
  argc_(argc),
  argv_(argv)
{
}

但我希望有一个默认构造函数Application::Application(),其中包含一些(很可能是)C代码,可以从某个地方获取argcargv


1
你说的“obtain”是什么意思?还有其他来源吗? - user3078414
11
除了 main 函数的参数之外,没有一种便携或标准的方式来获取程序的参数。 - Some programmer dude
4
@JoachimPileborg 是的,但问题是针对特定操作系统的,不可移植的。 - user1095108
14
你给这个问题打了“linux”、“windows”、“posix”和“bsd”的标签,并说这个问题是针对某个操作系统的?如果它是针对特定的操作系统,你只会提到一个操作系统,也就是你的目标操作系统。 - Some programmer dude
9
在正确理解问题之前,我们应该停止给格式不正确的问题点赞。对我来说,这个问题仍然不清楚,你基本上想获取那些参数,而无需传递它们?您可以创建一个包装器,如果注入,则间接提供参数。我认为一旦明确了提问者的要求,我们就可以验证他想要的是否可行。 - CoffeDeveloper
显示剩余4条评论
12个回答

37

在Linux上,您可以从进程的proc文件系统中获取此信息,即 /proc/$$/cmdline

int pid = getpid();
char fname[PATH_MAX];
char cmdline[ARG_MAX];
snprintf(fname, sizeof fname, "/proc/%d/cmdline", pid);
FILE *fp = fopen(fname);
fgets(cmdline, sizeof cmdline, fp);
// the arguments are in cmdline

7
  1. 你可以读取/proc/self/cmdline。
  2. 参数可能已被销毁。
  3. 使用FILE抽象处理/proc文件非常奇怪。
  4. ARG_MAX仅限制一个参数,而不是所有参数的总和。
  5. 缺少错误检查,在这个例子中可以省略。然而,最大的问题是OP很可能正在尝试做一些错误的事情,这样的回答应该在没有进一步澄清的情况下不应该首先发布。
- user4822941
2
我相信 /proc/*/cmdline 被截断到某个最大长度常量。这不是操作系统可以启动的进程命令行大小的限制。相反,这是 Linux 中进程列表记录保留的限制。因此,您可以使用更长的参数列表启动进程,但内核不会为了尝试从进程列表中读取参数而记住它们所有。 - Noah Spurrier
内核不会记住参数。相反,它会存储所述参数的开始和结束地址,并从目标进程地址空间中读取它们。我也没有看到任何截断结果的代码(除非请求的数量太小了)。http://lxr.free-electrons.com/source/fs/proc/base.c#L199 - user4822941
尝试使用此功能,但仅获取命令名称(如果明确指定,则包括路径)。因此,cat /proc/self/cmdline x y z 返回 cat/proc/self/cmdlinexyz,但对该文件的 fgets 仅返回 cat。为什么会这样? - Evgen
原来参数是以NULL分隔的。不能只使用字符串 :( - Evgen
这是一个非常巧妙的hack...一个惊人美丽、令人愉悦的hack! - Sergey Kalinichenko

30

main函数的参数由C运行时定义,这也是获取命令行参数的唯一标准/便携方式。不要与系统作对。 :)

如果您只想在程序的其他部分提供对命令行参数的访问,可以使用许多方法来实现自己的API。只需在main中使用argv/argc初始化自定义类,从那时起,您就可以忽略它们并使用自己的API。单例模式非常适合这种情况。

举个例子,最流行的C++框架之一Qt就使用了这种机制:

int main(int argc, char* argv[])
{
    QCoreApplication app(argc, argv);

    std::cout << app.arguments().at(0) << std::endl;

    return app.exec();
}

这些参数被应用程序捕获并复制到一个QStringList中。有关更多详细信息,请参见QCoreApplication :: arguments()

同样,在Mac上,Cocoa具有一种特殊的函数,它捕获命令行参数并使它们对框架可用:

#import <Cocoa/Cocoa.h>

int main(int argc, char *argv[])
{
    return NSApplicationMain(argc, (const char **)argv);
}

使用NSProcessInfo.arguments属性,应用程序中的参数随时可用。

我注意到在您更新的问题中,您的类直接在其实例中存储了argc/argv的副本:

int const argc_;
char const** const argv_;
虽然这样做应该是安全的(argv指针的生命周期应该在进程的整个生命周期内有效),但这并不符合C++的风格。考虑创建一个字符串向量(std::vector<std::string>)作为容器并复制字符串。然后它们甚至可以安全地进行可变操作(如果您想要的话!)
我希望创建一个独立于main()函数的类,这样就不必明确传递argc和argv给使用它们的代码。
目前不清楚为什么从main传递这些信息会被视为需要避免的坏事情。这正是大型框架的工作方式。
建议考虑使用单例以确保只有一个Application类的实例。参数可以通过main传递,但其他代码无需知道或关心它们来自何处。
如果你真的想隐藏main的参数传递到你的Application构造函数中的事实,你可以使用宏来隐藏它们。

C语言中有类?不会吧! - sfdcfox
点赞。我通常看到的 API 想要 CL 访问以解析自己的参数(例如:QT)的典型方法就是要求 argv 和 argc。使用标准习语的好处在于有经验的程序员一眼就知道你在做什么。 - T.E.D.
我理解使用全局变量的想法(虽然不太美观,但确实能完成工作),但是为什么要强制唯一性呢?允许任何人创建一个虚假的参数集并将其传递有什么问题吗? - Matthieu M.
这正是我不想要的。使用替代方法查找argcargv可能是错误的,但为什么还要阻止人们这样做呢?除了我所描述的情况之外,还有其他情况似乎能够从中受益。 - user1095108
@MatthieuM。Singleton仅用于保留与其封装的数据相同的语义;有一组只读参数。不是强制要求。 - gavinb
显示剩余4条评论

18

我完全同意 @gavinb 和其他人的观点。您应该确实使用从 main 中获得的参数并将它们存储或在需要它们的地方传递它们。那是唯一可移植的方式。

但是,仅用于教育目的,在我的 OS X 上使用 clang 和 Linux 上的 gcc,以下内容对我有效:

#include <stdio.h>

__attribute__((constructor)) void stuff(int argc, char **argv)
{
    for (int i=0; i<argc; i++) {
        printf("%s: argv[%d] = '%s'\n", __FUNCTION__, i, argv[i]);
    }
}

int main(int argc, char **argv)
{
    for (int i=0; i<argc; i++) {
        printf("%s: argv[%d] = '%s'\n", __FUNCTION__, i, argv[i]);
    }
    return 0;
}

输出结果为:

$ gcc -std=c99 -o test test.c && ./test this will also get you the arguments
stuff: argv[0] = './test'
stuff: argv[1] = 'this'
stuff: argv[2] = 'will'
stuff: argv[3] = 'also'
stuff: argv[4] = 'get'
stuff: argv[5] = 'you'
stuff: argv[6] = 'the'
stuff: argv[7] = 'arguments'
main: argv[0] = './test'
main: argv[1] = 'this'
main: argv[2] = 'will'
main: argv[3] = 'also'
main: argv[4] = 'get'
main: argv[5] = 'you'
main: argv[6] = 'the'
main: argv[7] = 'arguments'

之所以如此,是因为stuff函数被标记为__attribute__((constructor)),这意味着在动态链接器加载当前库时会运行它。这意味着在主程序中,即使在main函数运行之前,stuff函数也将运行并获得类似的环境。因此,您能够获取参数。

但是请注意:这仅用于教育目的,不应在任何生产代码中使用。它不具备可移植性,并且可能随时无预警地崩溃。


5
非常棒。__attribute__((constructor)) 函数是否保证能够访问 argv/argc,还是只是利用了某种表示的偶然性? - Patrick Collins
这完全没有保证。 - Johannes Weiss
1
glibc和dyld实现了这个扩展;musl没有。你可以根据需要进行考虑。 - saagarjha

16

就 Windows 部分而言,命令行可以通过 GetCommandLine 函数获得,该函数的文档在 此处,无需显式访问 main 函数的参数即可获取。


6
这是一个常见的答案。你可以考虑在行内添加一个如何使用 GetCommandLine的示例。 - Cory Klein
2
仅需使用__argc, __argv和__wargv - isanae

5
传递值并不构成创建依赖关系。您的类并不关心这些argcargv值来自哪里,它只想要它们被传递。但是您可能需要将这些值复制到其他地方 - 不能保证它们不会更改(同样适用于其他方法,例如GetCommandLine)。
事实上恰恰相反,使用像GetCommandLine这样的东西会创建隐藏的依赖性。突然间,你不再有一个简单的“传递值”的语义,而是“神奇地从其他地方获取他们的输入” - 再加上前面提到的“这些值随时可能发生变化”,这使得您的代码变得更加脆弱,更不可能进行测试。而解析命令行参数绝对是自动化测试非常有益的情况之一。如果您愿意,这就像全局变量与方法参数的方法。

但是OP指出这是C语言,所以没有类。您可以将它们传递给init函数,并将值存储在模块代码中的静态变量中。 - jamesqf
@jamesqf 嗯,我只是使用了原始帖子中使用的相同名称 :) 传递值是重要的部分,实现的具体方式并不重要。 - Luaan

5

在Windows中,如果您需要将参数作为wchar_t *获取,可以使用CommandLineToArgvW()函数:

int main()
{
    LPWSTR *sz_arglist;
    int n_args;
    int result;
    sz_arglist = CommandLineToArgvW(GetCommandLineW(), &n_args);
    if (sz_arglist == NULL)
    {
        fprintf(stderr, _("CommandLineToArgvW() failed.\n"));
        return 1;
    }
    else
    {
        result = wmain(n_args, sz_arglist);
    }
    LocalFree(sz_arglist);
    return result;
}

当使用MinGW时,这非常方便,因为gcc不把int _wmain(int, wchar_t *)识别为有效的main原型。


3
在C/C ++中,如果 main()不导出它们,则没有直接访问它们的方法;但是,这并不意味着没有间接的方法。许多类似Posix的系统使用elf格式,在栈上传递argcargvenvp以便通过常规调用约定初始化_start()并传递到main()中。这通常在汇编中完成(因为仍然没有可移植的方法来获取堆栈指针),并放在“开始文件”中,通常带有一些名称变体crt.o。
如果您无法访问 main()以便只需导出符号,则您可能不会访问_start()。那么,我为什么还要提到它呢?因为有第三个参数envp。由于environ是标准导出的变量,它使用envp在_start()中设置。在许多ELF系统中,如果您获取 environ的基地址并使用负数组索引向后移动,就可以推断出argcargv参数。第一个应该是NULL,然后是最后一个argv参数,直到您到达第一个。当指向的值强制转换为long时等于负索引的负数时,您就有了argc和下一个(比您的负索引多一)是argv / argv [0]。

我用FreePascal编写了一个Android应用程序,并收到报告称它在Android 13上崩溃。在Pascal中,有一个函数可以随时调用以获取args。通常,FreePascal会在其主函数中复制args。但是在Android上,它不是可执行文件,而是由JVM加载的.so文件,因此无法访问args。因此,FreePascal必须稍后搜索args,并通过从“environ”向后遍历来实现这一点。显然,这导致了崩溃。这是一个相当糟糕的想法。 - BeniBela

2
最便携的方法是使用全局变量存储参数。您可以通过使用单例(像问题中的类一样,但由main初始化)或类似的服务定位器来使其更加简洁:在main中创建一个对象,静态地传递和存储参数,并让另一个或同一个类访问它们。
非便携式的方法包括在Windows中使用GetCommandLine、访问/proc/<pid>/cmdline或(/proc/self/cmdline),或使用编译器特定的扩展,如__attribute__((constructor)) 请注意,在函数中获取命令行的等效方式,类似于GetCommandLine不可能(TLDR:命令行未传递给Linux内核,而是由调用进程(例如shell)解析和分割)。

实际上,在Windows上,据我所知,进程的命令行作为字符串传递给内核。Win32 API函数族CreateProcess接受一个字符串,并似乎通过几个其他函数将其传递到原始系统调用中而不加修改。_exec和类似函数实际上将它们接受的向量组合成单个字符串(!)甚至不费心引用它(!!!),然后将其传递给CreateProcess。 - Roflcopter4
我发的链接是关于Linux的。我编辑了答案以澄清这一点,谢谢你提供的信息! - Flamefire

2

我知道一些需要 int argc, char *argv[] 类型参数的函数常见场景。其中一个明显的例子是 GLUT,它的初始化函数从 main() 中获取这些参数,这种情况有点像“嵌套的 main”。这可能是您所期望的行为,也可能不是。如果不是,由于没有约定命名这些参数,只要您的函数具有其自己的参数解析器,并且您知道自己在做什么,您可以硬编码实现任何所需功能:

int foo = 1;
char * bar[1] = {" "};

或者从用户输入中读取,或以其他方式生成,据我所知。
int myFunc( int foo, char *bar[]){
//argument parser
 {…   …}
return 0;
}

请查看这个SO帖子,它与C语言中如何解析命令行参数有关。

2

这是一种正确的C++实现方式:

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <sstream>
#include <vector>

using namespace std;


template <typename T>
string to_str(T value ){
    ostringstream ss;
    ss << value;
    return ss.str();
    }

int main(int argc, char** argv){
    ifstream reader("/proc/" + to_str(getpid()) + "/cmdline", ios::binary);
    vector<unsigned char> buffer(istreambuf_iterator<char>(reader), {});
    
    int length = buffer.size();
    for(int i = 0; i < length; i++){
        if(!buffer[i]){
            cout << endl;
            }else{cout << buffer[i];}
        }
    
    return 0;
    }

重要的是不仅要发布代码,还要包括代码的描述以及为什么建议使用它。这有助于他人理解代码的上下文和目的,并使其对阅读问题或答案的其他人更有用。 - DSDmark
代码只是对@fluter的C答案的解释,以C++编写,因为问题明显要求使用C++方式。其余部分在原回答中已经解释过了。 - Василий максимов

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接