├── hello_sudo.txt ├── CHANGELOG.md ├── lib ├── shell.dart └── src │ ├── wrapped_process.dart │ └── shell.dart ├── pubspec.yaml ├── LICENSE ├── example └── main.dart ├── README.md └── .gitignore /hello_sudo.txt: -------------------------------------------------------------------------------- 1 | hello, admin! 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | * Migrate to null-safety 3 | 4 | # 1.0.1 5 | * Add runInShell attribute for Shell class 6 | 7 | # 1.0.0 8 | * First version. :dart: 9 | -------------------------------------------------------------------------------- /lib/shell.dart: -------------------------------------------------------------------------------- 1 | /// Wrapper over `dart:io` [Process] API's that supports features like environment management, user switches, and more. 2 | library shell; 3 | 4 | export 'src/shell.dart'; 5 | export 'src/wrapped_process.dart'; 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: shell 2 | version: 2.0.0 3 | homepage: https://github.com/thosakwe/shell.git 4 | author: Tobe Osakwe 5 | description: Wrapper over `dart:io` [Process] API's that supports additional features. 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | dependencies: 9 | file: ^6.1.2 10 | path: ^1.8.0 11 | platform: ^3.0.0 12 | process: ^4.2.3 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tobe O 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:file/local.dart'; 3 | import 'package:shell/shell.dart'; 4 | 5 | main() async { 6 | var fs = const LocalFileSystem(); 7 | var shell = new Shell(); 8 | var password = Platform.environment['PASSWORD']; 9 | print('Password: $password'); 10 | 11 | // Pipe results to files, easily. 12 | var echo = await shell.start('echo', arguments: ['hello world']); 13 | await echo.stdout.writeToFile(fs.file('hello.txt')); 14 | await echo.stderr.drain(); 15 | 16 | // You can run a program, and expect a certain exit code. 17 | // 18 | // If a valid exit code is returned, stderr is drained, and 19 | // you don't have to manually. 20 | // 21 | // Otherwise, a StateError is thrown. 22 | var find = await shell.start('find', arguments: ['.']); 23 | await find.expectExitCode([0]); // Can also call find.expectZeroExit() 24 | 25 | // Dump outputs. 26 | print(await find.stdout.readAsString()); 27 | 28 | // You can also run a process and immediately receive a string. 29 | var pwd = await shell.startAndReadAsString('pwd', arguments: []); 30 | print('cwd: $pwd'); 31 | 32 | // Navigation allows you to `cd`. 33 | shell.navigate('./lib/src'); 34 | pwd = await shell.startAndReadAsString('pwd', arguments: []); 35 | print('cwd: $pwd'); 36 | 37 | // We can make a separate shell, with the same settings. 38 | var forked = new Shell.copy(shell) 39 | ..sudo = true 40 | ..password = password; 41 | 42 | // Say hi, as an admin! 43 | var superEcho = await forked.start('echo', arguments: ['hello, admin!']); 44 | await superEcho.expectExitCode([0, 1]); 45 | await superEcho.stdout.writeToFile(fs.file('hello_sudo.txt')); 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shell 2 | [![Pub](https://img.shields.io/pub/v/shell.svg)](https://pub.dartlang.org/packages/shell) 3 | 4 | Wrapper over `dart:io` [Process] API's that supports features like environment management, user switches, and more. 5 | 6 | Useful for writing shell utility scripts in Dart, or within applications that perform system administration 7 | tasks on behalf of other users. 8 | 9 | ```dart 10 | import 'dart:io'; 11 | import 'package:file/local.dart'; 12 | import 'package:shell/shell.dart'; 13 | 14 | main() async { 15 | var fs = const LocalFileSystem(); 16 | var shell = new Shell(); 17 | var password = Platform.environment['PASSWORD']; 18 | print('Password: $password'); 19 | 20 | // Pipe results to files, easily. 21 | var echo = await shell.start('echo', ['hello world']); 22 | await echo.stdout.writeToFile(fs.file('hello.txt')); 23 | await echo.stderr.drain(); 24 | 25 | // You can run a program, and expect a certain exit code. 26 | // 27 | // If a valid exit code is returned, stderr is drained, and 28 | // you don't have to manually. 29 | // 30 | // Otherwise, a StateError is thrown. 31 | var find = await shell.start('find', ['.']); 32 | await find.expectExitCode([0]); // Can also call find.expectZeroExit() 33 | 34 | // Dump outputs. 35 | print(await find.stdout.readAsString()); 36 | 37 | // You can also run a process and immediately receive a string. 38 | var pwd = await shell.startAndReadAsString('pwd', []); 39 | print('cwd: $pwd'); 40 | 41 | // Navigation allows you to `cd`. 42 | shell.navigate('./lib/src'); 43 | pwd = await shell.startAndReadAsString('pwd', []); 44 | print('cwd: $pwd'); 45 | 46 | // We can make a separate shell, with the same settings. 47 | var forked = new Shell.copy(shell) 48 | ..sudo = true 49 | ..password = password; 50 | 51 | // Say hi, as an admin! 52 | var superEcho = await forked.start('echo', ['hello, admin!']); 53 | await superEcho.expectExitCode([0, 1]); 54 | await superEcho.stdout.writeToFile(fs.file('hello_sudo.txt')); 55 | } 56 | ``` -------------------------------------------------------------------------------- /lib/src/wrapped_process.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io' show BytesBuilder, Process; 4 | import 'package:file/file.dart'; 5 | 6 | class WrappedProcess { 7 | final Process process; 8 | final String executable; 9 | final Iterable arguments; 10 | late WrappedProcessOutput _stdout, _stderr; 11 | 12 | WrappedProcess(this.executable, this.arguments, this.process) { 13 | _stdout = new WrappedProcessOutput(process, process.stdout); 14 | _stderr = new WrappedProcessOutput(process, process.stderr); 15 | } 16 | 17 | Future get exitCode => process.exitCode; 18 | 19 | String get invocation => 20 | arguments.isEmpty ? executable : '$executable ${arguments.join(' ')}'; 21 | 22 | Future expectZeroExit() => expectExitCode(const [0]); 23 | 24 | Future expectExitCode(Iterable acceptedCodes) async { 25 | var code = await exitCode; 26 | 27 | if (!acceptedCodes.contains(code)) 28 | throw new StateError( 29 | '$invocation terminated with unexpected exit code $code.'); 30 | else 31 | await stderr.drain(); 32 | } 33 | 34 | int get pid => process.pid; 35 | 36 | IOSink get stdin => process.stdin; 37 | 38 | WrappedProcessOutput get stdout => _stdout; 39 | 40 | WrappedProcessOutput get stderr => _stderr; 41 | } 42 | 43 | class WrappedProcessOutput extends Stream> { 44 | final Process _process; 45 | final Stream> _stream; 46 | 47 | WrappedProcessOutput(this._process, this._stream); 48 | 49 | @override 50 | StreamSubscription> listen(void Function(List event)? onData, 51 | {Function? onError, void Function()? onDone, bool? cancelOnError}) { 52 | return _stream.listen(onData, 53 | onError: onError, onDone: onDone, cancelOnError: cancelOnError); 54 | } 55 | 56 | Future> readAsBytes() { 57 | return fold(new BytesBuilder(), (bb, buf) => bb..add(buf)) 58 | .then((bb) => bb.takeBytes()); 59 | } 60 | 61 | Future readAsString({Encoding encoding: utf8}) { 62 | return transform(encoding.decoder).join(); 63 | } 64 | 65 | Future writeToFile(File file) { 66 | return _process.exitCode.then((_) => _stream.pipe(file.openWrite())); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .packages 5 | .pub/ 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | ### Dart template 14 | # See https://www.dartlang.org/tools/private-files.html 15 | 16 | # Files and directories created by pub 17 | 18 | # SDK 1.20 and later (no longer creates packages directories) 19 | 20 | # Older SDK versions 21 | # (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20) 22 | .project 23 | .buildlog 24 | **/packages/ 25 | 26 | 27 | # Files created by dart2js 28 | # (Most Dart developers will use pub build to compile Dart, use/modify these 29 | # rules if you intend to use dart2js directly 30 | # Convention is to use extension '.dart.js' for Dart compiled to Javascript to 31 | # differentiate from explicit Javascript files) 32 | *.dart.js 33 | *.part.js 34 | *.js.deps 35 | *.js.map 36 | *.info.json 37 | 38 | # Directory created by dartdoc 39 | 40 | # Don't commit pubspec lock file 41 | # (Library packages only! Remove pattern if developing an application package) 42 | ### JetBrains template 43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 45 | 46 | # User-specific stuff: 47 | .idea/**/workspace.xml 48 | .idea/**/tasks.xml 49 | .idea/dictionaries 50 | 51 | # Sensitive or high-churn files: 52 | .idea/**/dataSources/ 53 | .idea/**/dataSources.ids 54 | .idea/**/dataSources.xml 55 | .idea/**/dataSources.local.xml 56 | .idea/**/sqlDataSources.xml 57 | .idea/**/dynamic.xml 58 | .idea/**/uiDesigner.xml 59 | 60 | # Gradle: 61 | .idea/**/gradle.xml 62 | .idea/**/libraries 63 | 64 | # Mongo Explorer plugin: 65 | .idea/**/mongoSettings.xml 66 | 67 | ## File-based project format: 68 | *.iws 69 | 70 | ## Plugin-specific files: 71 | 72 | # IntelliJ 73 | /out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Crashlytics plugin (for Android Studio and IntelliJ) 82 | com_crashlytics_export_strings.xml 83 | crashlytics.properties 84 | crashlytics-build.properties 85 | fabric.properties 86 | 87 | hello.txt 88 | hello_admin.txt -------------------------------------------------------------------------------- /lib/src/shell.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'package:path/path.dart' as p; 5 | import 'package:process/process.dart'; 6 | import 'wrapped_process.dart'; 7 | 8 | /// Wrapper over `package:process` [Process] API's that supports features like environment management, user switches, and more. 9 | class Shell { 10 | final ProcessManager processManager; 11 | final Map environment = {}; 12 | bool includeParentEnvironment, sudo; 13 | String? workingDirectory; 14 | String? username, password; 15 | bool runInShell; 16 | 17 | Shell( 18 | {this.processManager: const LocalProcessManager(), 19 | this.includeParentEnvironment: true, 20 | this.workingDirectory, 21 | this.sudo: false, 22 | this.runInShell: true, 23 | this.username, 24 | this.password, 25 | Map environment: const {}}) { 26 | this.environment.addAll(environment); 27 | workingDirectory ??= p.absolute(p.current); 28 | } 29 | 30 | factory Shell.copy(Shell other) { 31 | return new Shell( 32 | environment: other.environment, 33 | processManager: other.processManager, 34 | includeParentEnvironment: other.includeParentEnvironment, 35 | workingDirectory: other.workingDirectory, 36 | sudo: other.sudo, 37 | runInShell: other.runInShell, 38 | username: other.username, 39 | password: other.password); 40 | } 41 | 42 | void navigate(String path) { 43 | if (workingDirectory == null) { 44 | workingDirectory = path; 45 | } else { 46 | workingDirectory = p.join(workingDirectory!, path); 47 | } 48 | 49 | workingDirectory = p.absolute(workingDirectory!); 50 | } 51 | 52 | Future run(String executable, 53 | {Iterable arguments = const []}) { 54 | var command = [executable]..addAll(arguments); 55 | if (sudo) 56 | throw new UnsupportedError( 57 | 'When using `sudo`, you cannot call `run`, as stdin access is required to provide an account password.'); 58 | return processManager.run(command, 59 | workingDirectory: workingDirectory, 60 | environment: environment, 61 | runInShell: runInShell, 62 | includeParentEnvironment: includeParentEnvironment); 63 | } 64 | 65 | Future start(String executable, 66 | {Iterable arguments = const []}) async { 67 | var command = [executable]..addAll((arguments)); 68 | 69 | if (sudo || username != null) { 70 | // sudo -k -p '' 71 | var sudoArgs = ['sudo', '-k', '-p', '']; 72 | if (username != null) sudoArgs.addAll(['-u', username!]); 73 | if (password != null) sudoArgs.add('-S'); 74 | command.insertAll(0, sudoArgs); 75 | } 76 | 77 | var p = await processManager.start(command, 78 | workingDirectory: workingDirectory, 79 | environment: environment, 80 | runInShell: runInShell, 81 | includeParentEnvironment: includeParentEnvironment); 82 | if ((sudo || username != null) && password != null) 83 | p.stdin.writeln(password); 84 | return new WrappedProcess(command.first, command.skip(1), p); 85 | } 86 | 87 | Future startAndReadAsString(String executable, 88 | {Iterable arguments = const [], 89 | Encoding encoding: utf8, 90 | List acceptedExitCodes: const [0]}) async { 91 | var p = await start(executable, arguments: arguments); 92 | await p.expectExitCode(acceptedExitCodes); 93 | return await p.stdout.readAsString(encoding: encoding); 94 | } 95 | } 96 | --------------------------------------------------------------------------------