├── .github └── FUNDING.yml ├── .gitignore ├── .rtx.toml ├── LICENSE.md ├── Makefile ├── NOTICE ├── README.md ├── getopt_LICENSE.txt ├── rebar.config ├── rebar.lock ├── src ├── getopt.erl ├── grapherl.app.src └── grapherl.erl └── test └── grapherl_tests.hrl /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [eproxus] 4 | liberapay: eproxus 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _build 3 | _checkouts 4 | _vendor 5 | .eunit 6 | *.o 7 | *.beam 8 | *.plt 9 | *.swp 10 | *.swo 11 | .erlang.cookie 12 | ebin 13 | log 14 | erl_crash.dump 15 | .rebar 16 | logs 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | *~ 21 | doc 22 | -------------------------------------------------------------------------------- /.rtx.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | erlang = '24' 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | 3 | _Version 2.0, January 2004_ 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | ## Terms and Conditions for use, reproduction, and distribution 8 | 9 | ### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | ### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | ### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | ### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | ### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | ### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | ### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | ### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | ### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ## APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | > Copyright 2023 Adam Lindberg 183 | > 184 | > Copyright 2010 Erlang Solutions Ltd. 185 | > 186 | > Licensed under the Apache License, Version 2.0 (the "License"); 187 | > you may not use this file except in compliance with the License. 188 | > You may obtain a copy of the License at 189 | > 190 | > http://www.apache.org/licenses/LICENSE-2.0 191 | > 192 | > Unless required by applicable law or agreed to in writing, software 193 | > distributed under the License is distributed on an "AS IS" BASIS, 194 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 195 | > See the License for the specific language governing permissions and 196 | > limitations under the License. 197 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @rebar3 compile escriptize 3 | 4 | test: force 5 | @rebar3 eunit 6 | 7 | clean: 8 | @rebar3 clean 9 | 10 | force: ; 11 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Adam Lindberg 2 | 3 | Copyright 2011 Erlang Solutions 4 | This product contains code developed at Erlang Solutions. 5 | (http://www.erlang-solutions.com/) 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | grapherl 2 | ======== 3 | Create graphs of Erlang systems and programs. 4 | 5 | Getting Started 6 | --------------- 7 | 8 | First, install graphviz. On Ubuntu: 9 | 10 | $ sudo aptitude install graphviz 11 | 12 | On OS X, download and install the [OS X version of graphviz][1] or use 13 | [homebrew][2]: 14 | 15 | $ brew install graphviz 16 | 17 | To compile grapherl, type: 18 | 19 | $ make 20 | 21 | or the equivalent `./rebar compile`. 22 | 23 | To start a grapherl shell after compilation, type: 24 | 25 | $ erl -pa ebin 26 | 27 | Alternatively, compile a grapherl stand-alone executable by doing: 28 | 29 | $ ./rebar escriptize 30 | 31 | This will produce a `grapherl` executable in the root directory. Use 32 | the flags `-h` or `--help` to see wich arguments it needs. 33 | 34 | Examples 35 | -------- 36 | Here's some examples of using grapherl. 37 | 38 | The following two calls are equal. They will both generate 39 | `my_app.png` in the current directory. 40 | 41 | Eshell V5.7.5 (abort with ^G) 42 | 1> grapherl:modules("/path/to/my_app", "my_app"). 43 | ok 44 | 2> grapherl:modules("/path/to/my_app/ebin", "my_app", [no_ebin]). 45 | ok 46 | 47 | For example, if you have an Erlang release in the folder `my_node`, 48 | you can create a application dependency graph in SVG format by doing 49 | the following: 50 | 51 | Eshell V5.7.5 (abort with ^G) 52 | 1> grapherl:applications("/path/to/my_node/lib", "my_node", [{type, svg}]). 53 | ok 54 | 55 | This will create `my_node.svg` in the current directory. 56 | 57 | Tips 58 | --- 59 | 60 | If you're using Gnome under Linux, use the option `{open, 61 | "gnome-open"}` to directly see the resulting image. 62 | 63 | If you're using OS X, use the option `{open, "open"}`. 64 | 65 | Contribute 66 | ---------- 67 | 68 | Should you find yourself using grapherl and have issues, comments or 69 | feedback please [create an issue!][3] 70 | 71 | Patches are greatly appreciated! 72 | 73 | [1]: http://www.pixelglow.com/graphviz/ "graphviz for OS X" 74 | [2]: https://github.com/mxcl/homebrew "The missing package manager for OS" 75 | [3]: http://github.com/eproxus/grapherl/issues "grapherl issue tracker" 76 | -------------------------------------------------------------------------------- /getopt_LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Juan Jose Comellas 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | - Neither the name of Juan Jose Comellas nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [fail_on_warning, debug_info, {i, "test"}]}. 2 | {xref_checks, [undefined_function_calls]}. 3 | 4 | {cover_enabled, true}. 5 | {clean_files, [".eunit", "ebin/*.beam"]}. 6 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/getopt.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Juan Jose Comellas 3 | %%% @copyright (C) 2009 Juan Jose Comellas 4 | %%% @doc Parses command line options with a format similar to that of GNU getopt. 5 | %%% @end 6 | %%% 7 | %%% This source file is subject to the New BSD License. You should have received 8 | %%% a copy of the New BSD license with this software. If not, it can be 9 | %%% retrieved from: http://www.opensource.org/licenses/bsd-license.php 10 | %%%------------------------------------------------------------------- 11 | -module(getopt). 12 | -author('juanjo@comellas.org'). 13 | 14 | -export([parse/2, usage/2, usage/3, usage/4]). 15 | 16 | -export_type([arg_type/0, 17 | arg_value/0, 18 | arg_spec/0, 19 | simple_option/0, 20 | compound_option/0, 21 | option/0, 22 | option_spec/0]). 23 | 24 | -define(TAB_LENGTH, 8). 25 | %% Indentation of the help messages in number of tabs. 26 | -define(INDENTATION, 3). 27 | 28 | %% Position of each field in the option specification tuple. 29 | -define(OPT_NAME, 1). 30 | -define(OPT_SHORT, 2). 31 | -define(OPT_LONG, 3). 32 | -define(OPT_ARG, 4). 33 | -define(OPT_HELP, 5). 34 | 35 | -define(IS_OPT_SPEC(Opt), (tuple_size(Opt) =:= ?OPT_HELP)). 36 | 37 | 38 | %% Atom indicating the data type that an argument can be converted to. 39 | -type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'. 40 | %% Data type that an argument can be converted to. 41 | -type arg_value() :: atom() | binary() | boolean() | float() | integer() | string(). 42 | %% Argument specification. 43 | -type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined. 44 | %% Option type and optional default argument. 45 | -type simple_option() :: atom(). 46 | -type compound_option() :: {atom(), arg_value()}. 47 | -type option() :: simple_option() | compound_option(). 48 | %% Command line option specification. 49 | -type option_spec() :: { 50 | Name :: atom(), 51 | Short :: char() | undefined, 52 | Long :: string() | undefined, 53 | ArgSpec :: arg_spec(), 54 | Help :: string() | undefined 55 | }. 56 | 57 | 58 | %% @doc Parse the command line options and arguments returning a list of tuples 59 | %% and/or atoms using the Erlang convention for sending options to a 60 | %% function. 61 | -spec parse([option_spec()], string() | [string()]) -> 62 | {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. 63 | parse(OptSpecList, CmdLine) -> 64 | try 65 | Args = if 66 | is_integer(hd(CmdLine)) -> 67 | string:tokens(CmdLine, " \t\n"); 68 | true -> 69 | CmdLine 70 | end, 71 | parse(OptSpecList, [], [], 0, Args) 72 | catch 73 | throw: {error, {_Reason, _Data}} = Error -> 74 | Error 75 | end. 76 | 77 | 78 | -spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> 79 | {ok, {[option()], [string()]}}. 80 | %% Process the option terminator. 81 | parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) -> 82 | % Any argument present after the terminator is not considered an option. 83 | {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc, Tail)}}; 84 | %% Process long options. 85 | parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail]) -> 86 | parse_option_long(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); 87 | %% Process short options. 88 | parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail]) -> 89 | parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); 90 | %% Process non-option arguments. 91 | parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> 92 | case find_non_option_arg(OptSpecList, ArgPos) of 93 | {value, OptSpec} when ?IS_OPT_SPEC(OptSpec) -> 94 | parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], 95 | ArgAcc, ArgPos + 1, Tail); 96 | false -> 97 | parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail) 98 | end; 99 | parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) -> 100 | % Once we have completed gathering the options we add the ones that were 101 | % not present but had default arguments in the specification. 102 | {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}. 103 | 104 | 105 | %% @doc Parse a long option, add it to the option accumulator and continue 106 | %% parsing the rest of the arguments recursively. 107 | %% A long option can have the following syntax: 108 | %% --foo Single option 'foo', no argument 109 | %% --foo=bar Single option 'foo', argument "bar" 110 | %% --foo bar Single option 'foo', argument "bar" 111 | -spec parse_option_long([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> 112 | {ok, {[option()], [string()]}}. 113 | parse_option_long(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> 114 | case split_assigned_arg(OptArg) of 115 | {Long, Arg} -> 116 | % Get option that has its argument within the same string 117 | % separated by an equal ('=') character (e.g. "--port=1000"). 118 | parse_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg); 119 | 120 | Long -> 121 | case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of 122 | {Name, _Short, Long, undefined, _Help} -> 123 | parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args); 124 | 125 | {_Name, _Short, Long, _ArgSpec, _Help} = OptSpec -> 126 | % The option argument string is empty, but the option requires 127 | % an argument, so we look into the next string in the list. 128 | parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec); 129 | false -> 130 | throw({error, {invalid_option, OptStr}}) 131 | end 132 | end. 133 | 134 | 135 | %% @doc Parse an option where the argument is 'assigned' in the same string using 136 | %% the '=' character, add it to the option accumulator and continue parsing the 137 | %% rest of the arguments recursively. This syntax is only valid for long options. 138 | -spec parse_option_assigned_arg([option_spec()], [option()], [string()], integer(), 139 | [string()], string(), string(), string()) -> 140 | {ok, {[option()], [string()]}}. 141 | parse_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) -> 142 | case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of 143 | {_Name, _Short, Long, ArgSpec, _Help} = OptSpec -> 144 | case ArgSpec of 145 | undefined -> 146 | throw({error, {invalid_option_arg, OptStr}}); 147 | _ -> 148 | parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Args) 149 | end; 150 | false -> 151 | throw({error, {invalid_option, OptStr}}) 152 | end. 153 | 154 | 155 | %% @doc Split an option string that may contain an option with its argument 156 | %% separated by an equal ('=') character (e.g. "port=1000"). 157 | -spec split_assigned_arg(string()) -> {Name :: string(), Arg :: string()} | string(). 158 | split_assigned_arg(OptStr) -> 159 | split_assigned_arg(OptStr, OptStr, []). 160 | 161 | split_assigned_arg(_OptStr, "=" ++ Tail, Acc) -> 162 | {lists:reverse(Acc), Tail}; 163 | split_assigned_arg(OptStr, [Char | Tail], Acc) -> 164 | split_assigned_arg(OptStr, Tail, [Char | Acc]); 165 | split_assigned_arg(OptStr, [], _Acc) -> 166 | OptStr. 167 | 168 | 169 | %% @doc Parse a short option, add it to the option accumulator and continue 170 | %% parsing the rest of the arguments recursively. 171 | %% A short option can have the following syntax: 172 | %% -a Single option 'a', no argument or implicit boolean argument 173 | %% -a foo Single option 'a', argument "foo" 174 | %% -afoo Single option 'a', argument "foo" 175 | %% -abc Multiple options: 'a'; 'b'; 'c' 176 | %% -bcafoo Multiple options: 'b'; 'c'; 'a' with argument "foo" 177 | -spec parse_option_short([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> 178 | {ok, {[option()], [string()]}}. 179 | parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, [Short | Arg]) -> 180 | case lists:keyfind(Short, ?OPT_SHORT, OptSpecList) of 181 | {Name, Short, _Long, undefined, _Help} -> 182 | parse_option_short(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, Arg); 183 | 184 | {_Name, Short, _Long, ArgSpec, _Help} = OptSpec -> 185 | case Arg of 186 | [] -> 187 | % The option argument string is empty, but the option requires 188 | % an argument, so we look into the next string in the list. 189 | parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec); 190 | 191 | _ -> 192 | case is_valid_arg(ArgSpec, Arg) of 193 | true -> 194 | parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Args); 195 | _ -> 196 | parse_option_short(OptSpecList, [convert_option_no_arg(OptSpec) | OptAcc], ArgAcc, ArgPos, Args, OptStr, Arg) 197 | end 198 | end; 199 | 200 | false -> 201 | throw({error, {invalid_option, OptStr}}) 202 | end; 203 | parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, []) -> 204 | parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args). 205 | 206 | 207 | %% @doc Retrieve the argument for an option from the next string in the list of 208 | %% command-line parameters. 209 | parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail] = Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) -> 210 | % Special case for booleans: when the next string is an option we assume 211 | % the value is 'true'. 212 | case (arg_spec_type(ArgSpec) =:= boolean) andalso not is_boolean_arg(Arg) of 213 | true -> 214 | parse(OptSpecList, [{Name, true} | OptAcc], ArgAcc, ArgPos, Args); 215 | _ -> 216 | parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Tail) 217 | end; 218 | parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, [] = Args, {Name, _Short, _Long, ArgSpec, _Help}) -> 219 | % Special case for booleans: when the next string is missing we assume the 220 | % value is 'true'. 221 | case arg_spec_type(ArgSpec) of 222 | boolean -> 223 | parse(OptSpecList, [{Name, true} | OptAcc], ArgAcc, ArgPos, Args); 224 | _ -> 225 | throw({error, {missing_option_arg, Name}}) 226 | end. 227 | 228 | 229 | %% @doc Find the option for the discrete argument in position specified in the 230 | %% Pos argument. 231 | -spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false. 232 | find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) -> 233 | {value, OptSpec}; 234 | find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | Tail], Pos) -> 235 | find_non_option_arg(Tail, Pos - 1); 236 | find_non_option_arg([_Head | Tail], Pos) -> 237 | find_non_option_arg(Tail, Pos); 238 | find_non_option_arg([], _Pos) -> 239 | false. 240 | 241 | 242 | %% @doc Append options that were not present in the command line arguments with 243 | %% their default arguments. 244 | -spec append_default_options([option_spec()], [option()]) -> [option()]. 245 | append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) -> 246 | append_default_options(Tail, 247 | case lists:keymember(Name, 1, OptAcc) of 248 | false -> 249 | [{Name, DefaultArg} | OptAcc]; 250 | _ -> 251 | OptAcc 252 | end); 253 | %% For options with no default argument. 254 | append_default_options([_Head | Tail], OptAcc) -> 255 | append_default_options(Tail, OptAcc); 256 | append_default_options([], OptAcc) -> 257 | OptAcc. 258 | 259 | 260 | -spec convert_option_no_arg(option_spec()) -> compound_option(). 261 | convert_option_no_arg({Name, _Short, _Long, ArgSpec, _Help}) -> 262 | case ArgSpec of 263 | % Special case for booleans: if there is no argument we assume 264 | % the value is 'true'. 265 | {boolean, _DefaultValue} -> 266 | {Name, true}; 267 | boolean -> 268 | {Name, true}; 269 | _ -> 270 | throw({error, {missing_option_arg, Name}}) 271 | end. 272 | 273 | 274 | %% @doc Convert the argument passed in the command line to the data type 275 | %% indicated by the argument specification. 276 | -spec convert_option_arg(option_spec(), string()) -> compound_option(). 277 | convert_option_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg) -> 278 | try 279 | {Name, to_type(arg_spec_type(ArgSpec), Arg)} 280 | catch 281 | error:_ -> 282 | throw({error, {invalid_option_arg, {Name, Arg}}}) 283 | end. 284 | 285 | 286 | %% @doc Retrieve the data type form an argument specification. 287 | -spec arg_spec_type(arg_spec()) -> arg_type() | undefined. 288 | arg_spec_type({Type, _DefaultArg}) -> 289 | Type; 290 | arg_spec_type(Type) when is_atom(Type) -> 291 | Type. 292 | 293 | 294 | %% @doc Convert an argument string to its corresponding data type. 295 | -spec to_type(arg_type(), string()) -> arg_value(). 296 | to_type(binary, Arg) -> 297 | list_to_binary(Arg); 298 | to_type(atom, Arg) -> 299 | list_to_atom(Arg); 300 | to_type(integer, Arg) -> 301 | list_to_integer(Arg); 302 | to_type(float, Arg) -> 303 | list_to_float(Arg); 304 | to_type(boolean, Arg) -> 305 | LowerArg = string:to_lower(Arg), 306 | case is_arg_true(LowerArg) of 307 | true -> 308 | true; 309 | _ -> 310 | case is_arg_false(LowerArg) of 311 | true -> 312 | false; 313 | false -> 314 | erlang:error(badarg) 315 | end 316 | end; 317 | to_type(_Type, Arg) -> 318 | Arg. 319 | 320 | 321 | -spec is_arg_true(string()) -> boolean(). 322 | is_arg_true(Arg) -> 323 | (Arg =:= "true") orelse (Arg =:= "t") orelse 324 | (Arg =:= "yes") orelse (Arg =:= "y") orelse 325 | (Arg =:= "on") orelse (Arg =:= "enabled") orelse 326 | (Arg =:= "1"). 327 | 328 | 329 | -spec is_arg_false(string()) -> boolean(). 330 | is_arg_false(Arg) -> 331 | (Arg =:= "false") orelse (Arg =:= "f") orelse 332 | (Arg =:= "no") orelse (Arg =:= "n") orelse 333 | (Arg =:= "off") orelse (Arg =:= "disabled") orelse 334 | (Arg =:= "0"). 335 | 336 | 337 | -spec is_valid_arg(arg_spec(), nonempty_string()) -> boolean(). 338 | is_valid_arg({Type, _DefaultArg}, Arg) -> 339 | is_valid_arg(Type, Arg); 340 | is_valid_arg(boolean, Arg) -> 341 | is_boolean_arg(Arg); 342 | is_valid_arg(integer, Arg) -> 343 | is_integer_arg(Arg); 344 | is_valid_arg(float, Arg) -> 345 | is_float_arg(Arg); 346 | is_valid_arg(_Type, _Arg) -> 347 | true. 348 | 349 | 350 | -spec is_boolean_arg(string()) -> boolean(). 351 | is_boolean_arg(Arg) -> 352 | LowerArg = string:to_lower(Arg), 353 | is_arg_true(LowerArg) orelse is_arg_false(LowerArg). 354 | 355 | 356 | -spec is_integer_arg(string()) -> boolean(). 357 | is_integer_arg([Head | Tail]) when Head >= $0, Head =< $9 -> 358 | is_integer_arg(Tail); 359 | is_integer_arg([_Head | _Tail]) -> 360 | false; 361 | is_integer_arg([]) -> 362 | true. 363 | 364 | 365 | -spec is_float_arg(string()) -> boolean(). 366 | is_float_arg([Head | Tail]) when (Head >= $0 andalso Head =< $9) orelse Head =:= $. -> 367 | is_float_arg(Tail); 368 | is_float_arg([_Head | _Tail]) -> 369 | false; 370 | is_float_arg([]) -> 371 | true. 372 | 373 | 374 | %% @doc Show a message on stdout indicating the command line options and 375 | %% arguments that are supported by the program. 376 | -spec usage([option_spec()], string()) -> ok. 377 | usage(OptSpecList, ProgramName) -> 378 | io:format("Usage: ~s~s~n~n~s~n", 379 | [ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]). 380 | 381 | 382 | %% @doc Show a message on stdout indicating the command line options and 383 | %% arguments that are supported by the program. The CmdLineTail argument 384 | %% is a string that is added to the end of the usage command line. 385 | -spec usage([option_spec()], string(), string()) -> ok. 386 | usage(OptSpecList, ProgramName, CmdLineTail) -> 387 | io:format("Usage: ~s~s ~s~n~n~s~n", 388 | [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, usage_options(OptSpecList)]). 389 | 390 | 391 | %% @doc Show a message on stdout indicating the command line options and 392 | %% arguments that are supported by the program. The CmdLineTail and OptionsTail 393 | %% arguments are a string that is added to the end of the usage command line 394 | %% and a list of tuples that are added to the end of the options' help lines. 395 | -spec usage([option_spec()], string(), string(), [{string(), string()}]) -> ok. 396 | usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) -> 397 | UsageOptions = lists:foldl( 398 | fun ({Prefix, Help}, Acc) -> 399 | add_option_help(Prefix, Help, Acc) 400 | end, usage_options_reverse(OptSpecList, []), OptionsTail), 401 | io:format("Usage: ~s~s ~s~n~n~s~n", 402 | [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, 403 | lists:flatten(lists:reverse(UsageOptions))]). 404 | 405 | 406 | %% @doc Return a string with the syntax for the command line options and 407 | %% arguments. 408 | -spec usage_cmd_line([option_spec()]) -> string(). 409 | usage_cmd_line(OptSpecList) -> 410 | usage_cmd_line(OptSpecList, []). 411 | 412 | usage_cmd_line([{Name, Short, Long, ArgSpec, _Help} | Tail], Acc) -> 413 | CmdLine = 414 | case ArgSpec of 415 | undefined -> 416 | if 417 | % For options with short form and no argument. 418 | Short =/= undefined -> 419 | [$\s, $[, $-, Short, $]]; 420 | % For options with only long form and no argument. 421 | Long =/= undefined -> 422 | [$\s, $[, $-, $-, Long, $]]; 423 | true -> 424 | [] 425 | end; 426 | _ -> 427 | if 428 | % For options with short form and argument. 429 | Short =/= undefined -> 430 | [$\s, $[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]]; 431 | % For options with only long form and argument. 432 | Long =/= undefined -> 433 | [$\s, $[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]]; 434 | % For options with neither short nor long form and argument. 435 | true -> 436 | [$\s, $<, atom_to_list(Name), $>] 437 | end 438 | end, 439 | usage_cmd_line(Tail, [CmdLine | Acc]); 440 | usage_cmd_line([], Acc) -> 441 | lists:flatten(lists:reverse(Acc)). 442 | 443 | 444 | %% @doc Return a string with the help message for each of the options and 445 | %% arguments. 446 | -spec usage_options([option_spec()]) -> string(). 447 | usage_options(OptSpecList) -> 448 | lists:flatten(lists:reverse(usage_options_reverse(OptSpecList, []))). 449 | 450 | usage_options_reverse([{Name, Short, Long, _ArgSpec, Help} | Tail], Acc) -> 451 | Prefix = 452 | case Long of 453 | undefined -> 454 | case Short of 455 | % Neither short nor long form (non-option argument). 456 | undefined -> 457 | [$<, atom_to_list(Name), $>]; 458 | % Only short form. 459 | _ -> 460 | [$-, Short] 461 | end; 462 | _ -> 463 | case Short of 464 | % Only long form. 465 | undefined -> 466 | [$-, $- | Long]; 467 | % Both short and long form. 468 | _ -> 469 | [$-, Short, $,, $\s, $-, $- | Long] 470 | end 471 | end, 472 | usage_options_reverse(Tail, add_option_help(Prefix, Help, Acc)); 473 | usage_options_reverse([], Acc) -> 474 | Acc. 475 | 476 | 477 | %% @doc Add the help message corresponding to an option specification to a list 478 | %% with the correct indentation. 479 | -spec add_option_help(Prefix :: string(), Help :: string(), Acc :: string()) -> string(). 480 | add_option_help(Prefix, Help, Acc) when is_list(Help), Help =/= [] -> 481 | FlatPrefix = lists:flatten(Prefix), 482 | case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of 483 | TabSize when TabSize > 0 -> 484 | Tab = lists:duplicate(ceiling(TabSize / ?TAB_LENGTH), $\t), 485 | [[$\s, $\s, FlatPrefix, Tab, Help, $\n] | Acc]; 486 | _ -> 487 | % The indentation for the option description is 3 tabs (i.e. 24 characters) 488 | % IMPORTANT: Change the number of tabs below if you change the 489 | % value of the INDENTATION macro. 490 | [[$\t, $\t, $\t, Help, $\n], [$\s, $\s, FlatPrefix, $\n] | Acc] 491 | end; 492 | add_option_help(_Opt, _Prefix, Acc) -> 493 | Acc. 494 | 495 | 496 | 497 | %% @doc Return the smallest integral value not less than the argument. 498 | -spec ceiling(float()) -> integer(). 499 | ceiling(X) -> 500 | T = erlang:trunc(X), 501 | case (X - T) of 502 | % Neg when Neg < 0 -> 503 | % T; 504 | Pos when Pos > 0 -> 505 | T + 1; 506 | _ -> 507 | T 508 | end. 509 | -------------------------------------------------------------------------------- /src/grapherl.app.src: -------------------------------------------------------------------------------- 1 | {application, grapherl, 2 | [{description, "Create graphs of Erlang systems and programs."}, 3 | {vsn, "1.0"}, 4 | {registered, []}, 5 | {applications, [kernel, stdlib, tools]}, 6 | {env, []} 7 | ]}. 8 | -------------------------------------------------------------------------------- /src/grapherl.erl: -------------------------------------------------------------------------------- 1 | %% @author Adam Lindberg 2 | %% @doc Create graphs of Erlang systems and programs. 3 | %% 4 | %% Valid options are the following: 5 | %%
6 | %%
`type'
The type of the file as an atom. This can be 7 | %% all extensions that graphviz (`dot') supports. Default is `png'.
8 | %%
`open'
Command to run on resulting file as a 9 | %% string. This command will with the output file generated from 10 | %% `dot' as input.
11 | %%
`verbose'
Make `xref' verdbose. Default is `false'.
12 | %%
`warnings'
Make `xref' print warnings. Default is `false'
13 | %%
14 | -module(grapherl). 15 | 16 | -copyright("Erlang Solutions Ltd."). 17 | -author("Adam Lindberg "). 18 | 19 | -export([main/1]). 20 | -export([applications/2]). 21 | -export([applications/3]). 22 | -export([modules/2]). 23 | -export([modules/3]). 24 | 25 | -ifdef(TEST). 26 | -include("grapherl_tests.hrl"). 27 | -endif. 28 | 29 | %%============================================================================== 30 | %% API Functions 31 | %%============================================================================== 32 | 33 | %% @hidden 34 | main(Args) -> 35 | {ok, {Flags, _Rest} = Options} = getopt:parse(options(), Args), 36 | case lists:member(help, Flags) of 37 | true -> print_options(), halt(0); 38 | false -> run(Options) 39 | end. 40 | 41 | run({Options, [Dir, Target]}) -> 42 | case get_mode(Options) of 43 | {app, RestOpt} -> run(applications, [Dir, Target, RestOpt]); 44 | {mod, RestOpt} -> run(modules, [Dir, Target, RestOpt]) 45 | end; 46 | run({_Options, _Other}) -> 47 | print_options(), halt(1). 48 | 49 | get_mode(Options) -> 50 | case proplists:split(Options, [app, mod]) of 51 | {[[app], []], Rest} -> {app, Rest}; 52 | {[[], [mod]], Rest} -> {mod, Rest} 53 | end. 54 | 55 | options() -> 56 | [{help, $h, "help", undefined, 57 | "Display this help text"}, 58 | {mod, $m, "modules", undefined, 59 | "Analyse module dependencies (mutually exclusive)"}, 60 | {app, $a, "applications", undefined, 61 | "Analyse application dependencies (mutually exclusive)"}, 62 | {type, $t, "type", string, 63 | "Output file type (also deduced from file name)"}]. 64 | 65 | print_options() -> 66 | getopt:usage(options(), filename:basename(escript:script_name()), 67 | "SOURCE OUTPUT", 68 | [{"SOURCE", "The source directory to analyse"}, 69 | {"OUTPUT", "Target ouput file"}]). 70 | 71 | run(Fun, Args) -> 72 | try apply(?MODULE, Fun, Args) of 73 | ok -> 74 | halt(0); 75 | {error, Error} -> 76 | io:format("grapherl: error: ~p~n", [Error]), 77 | halt(2) 78 | catch 79 | error:type_not_specified -> 80 | io:format("grapherl: error: File type not specified~n"), 81 | halt(2) 82 | end. 83 | 84 | %% @equiv applications(Dir, Target, [{type, png}]) 85 | applications(Dir, Target) -> 86 | applications(Dir, Target, [{type, png}]). 87 | 88 | %% @doc Generate an application dependency graph based on function calls. 89 | %% 90 | %% `Dir' is the library directory of the release you want to graph. `Target' 91 | %5 is the target filename (without extension). 92 | applications(Dir, Target, Options) -> 93 | check_dot(), 94 | try 95 | initialize_xref(?MODULE, Options), 96 | ok(xref:add_release(?MODULE, Dir, {name, ?MODULE})), 97 | Excluded = ifc(proplists:is_defined(include_otp, Options), 98 | [], otp_apps()) 99 | ++ proplists:get_value(excluded, Options, []), 100 | {ok, Results} = xref:q(?MODULE, "AE"), 101 | Relations = [uses(F, T) || 102 | {F, T} <- Results, 103 | F =/= T, 104 | not lists:member(F, Excluded), 105 | not lists:member(T, Excluded)], 106 | create(["node [shape = tab];"] ++ Relations, Target, Options), 107 | stop_xref(?MODULE) 108 | catch 109 | throw:Error -> 110 | stop_xref(?MODULE), 111 | Error 112 | end. 113 | 114 | 115 | %% @equiv application(App, Target, [{type, png}]) 116 | modules(Dir, Target) -> 117 | modules(Dir, Target, []). 118 | 119 | %% @doc Generate a module dependency graph for an application. 120 | %% 121 | %% `Dir' is the directory of the application. `Target' is the target 122 | %% filename (without extension). 123 | %% 124 | %% All modules in the `ebin' folder in the directory specified in 125 | %% `Dir' will be included in the graph. The option `no_ebin' will, if 126 | %% set to true or just included as an atom, use the `Dir' directory as 127 | %% a direct source for .beam files. 128 | modules(Dir, Target, Options) -> 129 | %% TODO: Thickness of arrows could be number of calls? 130 | check_dot(), 131 | try 132 | initialize_xref(?MODULE, Options), 133 | Path = get_path(Dir), 134 | ok(xref:add_directory(?MODULE, Path)), 135 | Modules = case ok(xref:q(?MODULE, "AM")) of 136 | [] -> throw({error, no_modules_found}); 137 | Else -> Else 138 | end, 139 | Query = "ME ||| [" 140 | ++ string:join(["'" ++ atom_to_list(M) ++ "'" || M <- Modules], ",") 141 | ++ "]", 142 | {ok, Results} = xref:q(?MODULE, Query), 143 | Relations = [uses(F, T) || {F, T} <- Results, F =/= T], 144 | create(["node [shape = box];"] 145 | ++ [["\"" ++ atom_to_list(M) ++ "\"", $;] || M <- Modules] 146 | ++ Relations, Target, Options), 147 | stop_xref(?MODULE) 148 | catch 149 | throw:Error -> 150 | stop_xref(?MODULE), 151 | Error 152 | end. 153 | 154 | %%============================================================================== 155 | %% Internal Functions 156 | %%============================================================================== 157 | 158 | get_path(Dir) -> 159 | case filelib:wildcard(filename:join(Dir, "*.beam")) of 160 | [] -> filename:join(Dir, "ebin"); 161 | _Beams -> Dir 162 | end. 163 | 164 | initialize_xref(Name, Options) -> 165 | case xref:start(Name) of 166 | {error, {already_started, _}} -> 167 | stop_xref(Name), 168 | xref:start(Name); 169 | {ok, _Ref} -> 170 | ok 171 | end, 172 | XRefOpts = [{verbose, proplists:is_defined(verbose, Options)}, 173 | {warnings, proplists:is_defined(warnings, Options)}], 174 | ok = xref:set_default(Name, XRefOpts). 175 | 176 | stop_xref(Ref) -> 177 | xref:stop(Ref), 178 | ok. 179 | 180 | get_type(Options, Target) -> 181 | case proplists:get_value(type, Options) of 182 | undefined -> type_from_filename(Target); 183 | Type when is_atom(Type) -> atom_to_list(Type); 184 | Type -> Type 185 | end. 186 | 187 | type_from_filename(Filename) -> 188 | case filename:extension(Filename) of 189 | "" -> erlang:error(type_not_specified); 190 | "." ++ Type -> Type 191 | end. 192 | 193 | file(Lines) -> 194 | ["digraph application_graph {", Lines, "}"]. 195 | 196 | uses(From, To) -> 197 | ["\"" ++ atom_to_list(From) ++ "\"", " -> ", 198 | "\"" ++ atom_to_list(To) ++ "\"", $;]. 199 | 200 | create(Lines, Target, Options) -> 201 | case dot(file(Lines), Target, get_type(Options, Target)) of 202 | {ok, File} -> 203 | case proplists:get_value(open, Options) of 204 | undefined -> ok; 205 | Command -> os:cmd(Command ++ " " ++ File), ok 206 | end; 207 | {Error, _File} -> 208 | {error, hd(string:tokens(Error, "\n"))} 209 | end. 210 | 211 | check_dot() -> 212 | case os:cmd("dot -V") of 213 | "dot " ++ _ -> 214 | ok; 215 | _Else -> 216 | erlang:error("dot was not found, please install graphviz",[]) 217 | end. 218 | 219 | dot(File, Target, Type) -> 220 | TmpFile = string:strip(os:cmd("mktemp -t " ?MODULE_STRING ".XXXX"), both, $\n), 221 | ok = file:write_file(TmpFile, File), 222 | 223 | TargetName = add_extension(Target, Type), 224 | Result = case Type of 225 | "dot" -> file:write_file(TargetName, File); 226 | _ -> 227 | case os:cmd(io_lib:format("dot -T~p -o~p ~p", [Type, TargetName, TmpFile])) of 228 | "" -> ok; 229 | X -> X 230 | end 231 | end, 232 | {Result, TargetName}. 233 | 234 | add_extension(Target, Type) -> 235 | case filename:extension(Target) of 236 | "." ++ Type -> Target; 237 | _Else -> Target ++ "." ++ Type 238 | end. 239 | 240 | otp_apps() -> 241 | {ok, Apps} = file:list_dir(filename:join(code:root_dir(), "lib")), 242 | [list_to_atom(hd(string:tokens(A, "-"))) || A <- Apps]. 243 | 244 | ok({ok, Result}) -> Result; 245 | ok(Error) -> throw(Error). 246 | 247 | ifc(true, True, _) -> True; 248 | ifc(false, _, False) -> False. 249 | -------------------------------------------------------------------------------- /test/grapherl_tests.hrl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | 3 | get_mode_test_() -> 4 | {inparallel, [?_assertEqual({app, []}, get_mode([app])), 5 | ?_assertEqual({mod, []}, get_mode([mod])), 6 | ?_assertEqual({app, [other]}, get_mode([app, other])), 7 | ?_assertEqual({app, [other]}, get_mode([other, app]))]}. 8 | 9 | get_type_test_() -> 10 | {inparallel, [?_assertEqual("png", get_type([{type, png}], "")), 11 | ?_assertEqual("png", get_type([], "test.png")), 12 | ?_assertError(type_not_specified, get_type([], "test"))]}. 13 | 14 | add_extension_test_() -> 15 | {inparallel, [?_assertEqual("test.png", add_extension("test", "png")), 16 | ?_assertEqual("test.png", add_extension("test.png", "png"))]}. 17 | --------------------------------------------------------------------------------