From 1de10c4c3b177b5c56b2f1fd0981737ad1c52751 Mon Sep 17 00:00:00 2001 From: Devin Alexander Torres Date: Sun, 9 Jun 2024 06:50:10 -0500 Subject: [PATCH] WIP --- .credo.exs | 215 +++++++++++++++++++++++++++++++++++++++ .editorconfig | 9 +- .formatter.exs | 8 ++ .gitattributes | 1 + .github/workflows/ci.yml | 64 ++++++++++++ .gitignore | 28 ++++- .tool-versions | 2 + .travis.yml | 15 --- .vscode/settings.json | 4 + LICENSE | 14 +++ README.md | 18 ++-- UNLICENSE | 24 ----- config/config.exs | 22 ---- lib/sh.ex | 66 +++++++----- mix.exs | 63 +++++++----- mix.lock | 6 ++ test/sh_test.exs | 34 +++++-- test/test_helper.exs | 2 +- 18 files changed, 458 insertions(+), 137 deletions(-) create mode 100644 .credo.exs create mode 100644 .formatter.exs create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 .tool-versions delete mode 100644 .travis.yml create mode 100644 .vscode/settings.json create mode 100644 LICENSE delete mode 100644 UNLICENSE delete mode 100644 config/config.exs create mode 100644 mix.lock diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..419a21d --- /dev/null +++ b/.credo.exs @@ -0,0 +1,215 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "config/", + "lib/", + "test/", + "{.credo,.formatter,mix}.exs" + ], + excluded: [~r"/_build/", ~r"/deps/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [ + priority: :low, + if_nested_deeper_than: 2, + if_called_more_often_than: 0 + ]}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, + [priority: :low, max_length: 80]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.UtcNowTruncate, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.LazyLogging, false}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + ], + disabled: [ + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.Specs, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.editorconfig b/.editorconfig index 37d4548..959477f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,10 +5,9 @@ root = true # Unix-style newlines with a newline ending every file [*] -end_of_line = lf -insert_final_newline = true - -# 2 space indentation -[*.{ex,exs}] indent_style = space indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..5142c88 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,8 @@ +# Used by "mix format" +[ + line_length: 80, + inputs: [ + "{.credo,.formatter,mix}.exs", + "{config,lib,test}/**/*.{ex,exs}" + ] +] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..92fe0df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + +env: + MIX_ENV: test + +jobs: + test: + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + strategy: + matrix: + os: + - ubuntu-22.04 + - windows-2022 + otp: + - "27.0" + - "26.2.5.1" + - "25.3.2.12" + elixir: + - "1.17.1" + - "1.16.3" + - "1.15.8" + - "1.14.5" + exclude: + - otp: "27.0" + elixir: "1.16.3" + - otp: "27.0" + elixir: "1.15.8" + - otp: "27.0" + elixir: "1.14.5" + - otp: "27.0" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: erlef/setup-beam@v1.18 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + version-type: strict + + - uses: actions/cache@v4 + id: cache + with: + path: | + _build + deps + key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} + + - if: steps.cache.outputs.cache-hit != 'true' + run: mix deps.get + + - run: mix test diff --git a/.gitignore b/.gitignore index 9607671..fa85f31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,28 @@ -/_build -/deps +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). *.ez + +# Ignore package tarball (built via "mix hex.build"). +sh-*.tar + +# Temporary files, for example, from tests. +/tmp/ + +.elixir_ls diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..960884c --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 27.0 +elixir 1.17.1-otp-27 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0154fec..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: elixir -elixir: - - 1.2.0 - - 1.1.1 - - 1.1.0 - - 1.0.5 - - 1.0.4 -otp_release: - - 18.2 - - 18.1 - - 18.0 - - 17.5 - - 17.4 - - 17.3 -sudo: false diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dd65ea9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "elixirLS.enableTestLenses": true +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62b6630 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (C) 2024 Devin Alexander Torres + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index 276c9f2..1f99c0e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Sh -[![Build Status](https://api.travis-ci.org/devinus/sh.svg?branch=master)](https://travis-ci.org/devinus/sh) +[![Build Status](https://img.shields.io/github/actions/workflow/status/devinus/sh/ci.yml)](https://github.com/devinus/sh/actions/workflows/ci.yml) -[![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.png)](https://gratipay.com/devinus/) - -An Elixir module inspired by Python's [sh](http://amoffat.github.io/sh/) -package. `Sh` allows you to call any program as if it were a function. +An Elixir module inspired by Python's [sh](https://sh.readthedocs.io) package. +`Sh` allows you to call any program as if it were a function. ## Adding Sh to Your Project -To use Sh with your projects, simply edit your mix.exs file and add it as a dependency: + +To use Sh with your projects, simply edit your mix.exs file and add it as a Hex +dependency: ```elixir defp deps do @@ -19,7 +19,7 @@ To use Sh with your projects, simply edit your mix.exs file and add it as a depe ## Example ```iex -iex> Sh.echo "Hello World!" +iex> Sh.echo("Hello World!") "Hello World!\n" ``` @@ -28,14 +28,14 @@ iex> Sh.echo "Hello World!" `Sh` commands accept as the last argument a list of options. ```elixir -Sh.curl "http://example.com/", o: "page.html", silent: true +Sh.curl("http://example.com/", o: "page.html", silent: true) "" ``` The equivalent call without using this feature would be: ```elixir -Sh.curl "-o", "page.html", "--silent", "http://example.com/" +Sh.curl("-o", "page.html", "--silent", "http://example.com/") ``` ## Underscores diff --git a/UNLICENSE b/UNLICENSE deleted file mode 100644 index 68a49da..0000000 --- a/UNLICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index b477e92..0000000 --- a/config/config.exs +++ /dev/null @@ -1,22 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies. The Mix.Config module provides functions -# to aid in doing so. -use Mix.Config - -# Note this file is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. - -# Sample configuration: -# -# config :my_dep, -# key: :value, -# limit: 42 - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" diff --git a/lib/sh.ex b/lib/sh.ex index 4a591e1..3c9979c 100644 --- a/lib/sh.ex +++ b/lib/sh.ex @@ -1,9 +1,13 @@ defmodule Sh do + readme_path = [__DIR__, "..", "README.md"] |> Path.join() |> Path.expand() + @external_resource readme_path + @moduledoc readme_path |> File.read!() |> String.trim() + defmodule CommandNotFound do defexception [:command] def message(%{command: command}) do - "Command not found: #{command}" + "command not found: #{command}" end end @@ -16,41 +20,55 @@ defmodule Sh do end def unquote(:"$handle_undefined_function")(program, args) do - command = to_string(program) + command = + program + |> to_string() |> String.replace("_", "-") - |> System.find_executable - || raise CommandNotFound, command: program - - if is_list(List.last(args)) do - { args, [opts] } = Enum.split(args, -1) - args = process_options(opts) ++ args - end + |> System.find_executable() || + raise CommandNotFound, command: program - # http://www.erlang.org/doc/man/erlang.html#open_port-2 - opts = ~w(exit_status stderr_to_stdout in binary eof hide)a ++ [args: args] - port = Port.open({ :spawn_executable, command }, opts) + args = + if is_list(List.last(args)) do + {args, [opts]} = Enum.split(args, -1) + process_options(opts) ++ args + else + args + end - loop(port, "") + # See: https://www.erlang.org/doc/apps/erts/erlang.html#open_port/2 + opts = [args: args] ++ ~w(exit_status stderr_to_stdout in binary eof hide)a + port = Port.open({:spawn_executable, command}, opts) + IO.iodata_to_binary(loop(port, [])) end defp loop(port, acc) do receive do - { ^port, { :data, data } } -> - loop(port, acc <> data) - { ^port, :eof } -> - send port, { self, :close } + {^port, {:data, data}} -> + loop(port, [acc | data]) + + {^port, :eof} -> + send(port, {self(), :close}) + receive do - { ^port, { :exit_status, 0 } } -> + {^port, {:exit_status, 0}} -> acc - { ^port, { :exit_status, status } } -> - raise %AbnormalExit{output: acc, status: status} + + {^port, {:exit_status, status}} -> + output = IO.iodata_to_binary(acc) + raise %AbnormalExit{output: output, status: status} end + + {:EXIT, ^port, :normal} -> + acc end end defp process_options(opts) do - Enum.reduce Enum.reverse(opts), [], fn { key, value }, acc -> + opts + |> Enum.reverse() + |> Enum.reduce([], fn {key, value}, acc -> key = to_string(key) + if String.length(key) == 1 do if value do if value != true do @@ -63,9 +81,11 @@ defmodule Sh do end else key = String.replace(key, "_", "-") + if value do + # credo:disable-for-next-line Credo.Check.Refactor.Nesting if value != true do - ["--#{key}=#{value}" | acc] + ["-#{key}", to_string(value) | acc] else ["--#{key}" | acc] end @@ -73,6 +93,6 @@ defmodule Sh do acc end end - end + end) end end diff --git a/mix.exs b/mix.exs index 9b1c3a2..700a830 100644 --- a/mix.exs +++ b/mix.exs @@ -1,41 +1,54 @@ defmodule Sh.Mixfile do use Mix.Project - @version String.strip(File.read!("VERSION")) + version_path = Path.join([__DIR__, "VERSION"]) + + @external_resource version_path + @version version_path |> File.read!() |> String.trim() + @source_url "https://github.com/devinus/sh" def project do - [app: :sh, - version: @version, - elixir: "~> 1.0", - description: "Run programs as functions in Elixir", - deps: deps, - package: package] + [ + app: :sh, + version: @version, + elixir: "~> 1.14", + description: "Run programs as functions in Elixir", + source_url: @source_url, + deps: deps(), + docs: docs(), + package: package() + ] end - # Configuration for the OTP application - # - # Type `mix help compile.app` for more information + # Run "mix help compile.app" to learn about applications. def application do - [applications: []] + [] end - # Dependencies can be hex.pm packages: - # - # {:mydep, "~> 0.3.0"} - # - # Or git/path repositories: - # - # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1"} - # - # Type `mix help deps` for more examples and options + # Run "mix help deps" to learn about dependencies. defp deps do - [] + [ + {:credo, "~> 1.7.7", only: [:dev, :test], runtime: false} + ] + end + + defp docs do + [ + main: "Sh", + canonical: "https://hexdocs.pm/sh", + extras: [ + "README.md", + LICENSE: [title: "License"] + ] + ] end defp package do - [files: ~w(lib mix.exs README.md UNLICENSE VERSION), - maintainers: ["Devin Torres"], - licenses: ["Unlicense"], - links: %{"GitHub" => "https://github.com/devinus/sh"}] + [ + files: ~w(lib mix.exs README.md LICENSE VERSION), + maintainers: ["Devin Alexander Torres "], + licenses: ["0BSD"], + links: %{"GitHub" => @source_url} + ] end end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..50b3bee --- /dev/null +++ b/mix.lock @@ -0,0 +1,6 @@ +%{ + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, +} diff --git a/test/sh_test.exs b/test/sh_test.exs index 94d46b8..4ac6801 100644 --- a/test/sh_test.exs +++ b/test/sh_test.exs @@ -1,53 +1,65 @@ defmodule ShTest do - use ExUnit.Case + use ExUnit.Case, async: true + doctest Sh - alias Sh.CommandNotFound alias Sh.AbnormalExit + alias Sh.CommandNotFound @tmp_dir Path.expand("tmp", __DIR__) setup_all do File.mkdir_p!(@tmp_dir) - on_exit fn -> + on_exit(fn -> File.rm_rf!(@tmp_dir) :ok - end + end) :ok end test "simple commands" do - assert Sh.true == "" + assert Sh.true() == "" assert Sh.echo("Hello World!") == "Hello World!\n" assert Sh.ls(Path.expand("fixtures", __DIR__)) == "test.txt\n" assert Sh.cat(fixture_path("test.txt")) == "foo\nbar\nbaz\n" assert Sh.tail("-n1", fixture_path("test.txt")) == "baz\n" + + fun = &Sh.true/0 + assert fun.() == "" + + # credo:disable-for-next-line Credo.Check.Refactor.Apply + assert apply(Sh, true, []) == "" end test "commands with options" do assert Sh.echo("Hello World!", n: true) == "Hello World!" - assert Sh.curl("http://httpbin.org/html", o: tmp_path("page.html"), silent: true) == "" + + assert Sh.curl("http://httpbin.org/html", + o: tmp_path("page.html"), + silent: true + ) == "" + assert File.exists?(tmp_path("page.html")) end test "command not found" do assert_raise CommandNotFound, fn -> - Sh.kurvin + Sh.nonexistent_command() end end - test "non-zero exits" do + test "abnormal exit" do assert_raise AbnormalExit, fn -> - Sh.false + Sh.false() end end defp fixture_path(path) do - Path.expand(Path.join(["fixtures", path]), __DIR__) + ["fixtures", path] |> Path.join() |> Path.expand(__DIR__) end defp tmp_path(path) do - Path.expand(Path.join(["tmp", path]), __DIR__) + ["tmp", path] |> Path.join() |> Path.expand(__DIR__) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4b8b246..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start +ExUnit.start()